Compare commits

..

4 Commits
master ... prod

Author SHA1 Message Date
Flo
6da1b65915 prod: new devenv 2025-10-31 17:09:19 +01:00
Flo
7c62023e6f tag analyse v1 2024-08-10 17:07:40 +00:00
Flo
9e3e2ce15d tag sorting, and tag thumbnail 2024-08-07 19:40:40 +00:00
Flo
caff0ec71f analyze 2024-08-05 10:27:38 +00:00
57 changed files with 1054 additions and 161 deletions

View File

@ -1,10 +1,10 @@
# DB Configuration
DB_DRIVER=pdo_mysql
DB_HOST=myTube-backend-mysql
DB_HOST=mytube-backend-mysql
DB_PORT=3306
DB_USER=myTube
DB_USER=mytube
DB_PASSWORD=pass
DB_NAME=myTube
DB_NAME=mytube
DB_NAME_LOG=log
# MyTube Setup

View File

@ -1,3 +0,0 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/exec build

View File

@ -1,3 +0,0 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/exec down

View File

@ -1,23 +0,0 @@
#!/bin/bash
COMMAND="$@"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
#LOAD ENV VARIABLES FROM .ENV
export $(grep -v '^#' "${SCRIPT_DIR}/../../.env" | xargs)
#MAC
if [[ "$OSTYPE" == "darwin"* ]]; then
docker compose -f "${SCRIPT_DIR}/../../docker/docker-compose-mac.yml" $COMMAND
#LINUX
elif [[ "$OSTYPE" == "linux-gnu" ]]; then
docker compose -f "${SCRIPT_DIR}/../../docker/docker-compose.yml" $COMMAND
else
echo "Dieses Skript wird auf deinem Gerät nicht unterstützt"
exit 1
fi
#UNSET ENV VARIABLES FROM .ENV
unset $(grep -v '^#' "${SCRIPT_DIR}/../../.env" | sed -E 's/(.*)=.*/\1/' | xargs)

40
bin/script/firstRun Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
source $ENV_DIR/bin/messages
source $ENV_DIR/bin/drun
# Create local config files
infomsg '[backend] Creating local config...'
cp "$PROJECT_DIR"/config/autoload/monolog.local.php.dist "$PROJECT_DIR"/config/autoload/monolog.local.php
cp "$PROJECT_DIR"/config/autoload/doctrine.local.php.dist "$PROJECT_DIR"/config/autoload/doctrine.local.php
cp "$PROJECT_DIR"/config/autoload/redis.local.php.dist "$PROJECT_DIR"/config/autoload/redis.local.php
successmsg '[backend] Local config done'
# Install packages
infomsg '[backend] Composer install...'
drun backend composer install -o
successmsg '[backend] Composer install done'
# Dump autoload
infomsg "[backend]: Dump autoload"
drun backend composer da
successmsg "[backend]: Dump autoload done"
# Migrate Databases
infomsg '[backend] Running migrations...'
drun backend composer doctrine migrations:migrate --no-interaction
drun backend composer doctrine-log migrations:migrate --no-interaction
successmsg '[backend] Migrations done'
# Seed DB
infomsg '[backend] Running seeder...'
drun backend composer console init:data
drun backend composer console rbac:update
successmsg '[backend] Seeding done'
successmsg '[backend] Initialization script done'

View File

@ -2,31 +2,19 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
source $ENV_DIR/bin/messages
# Check .env file
if [ ! -f "$PROJECT_DIR/.env" ]
then
echo "Create .env file from example..."
if [ ! -f "$PROJECT_DIR/.env" ] ; then
infomsg "[backend] Creating .env"
cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env"
echo ".env file created, please change variables and call me again"
exit 1
successmsg "[backend] Created .env"
fi
# Build and start docker containers
$SCRIPT_DIR/exec build
$SCRIPT_DIR/exec up -d
# Source drun
source $ENV_DIR/bin/script/drun
# Install PHP packages
drun myTube-backend composer install
# Migrate databases to current version
drun myTube-backend composer dmm
drun myTube-backend composer dmlm
# Insert setup for project after this line
drun myTube-backend composer console rbac:update
drun myTube-backend composer console init:data
# Check docker-compose.yml file
if [ ! -f "$PROJECT_DIR/docker/docker-compose.yml" ] ; then
infomsg "[backend] Creating docker-compose.yml"
cp "$PROJECT_DIR/docker/docker-compose.yml.dist" "$PROJECT_DIR/docker/docker-compose.yml"
successmsg "[backend] Creating docker-compose.yml"
fi

View File

@ -1,3 +0,0 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/exec stop

View File

@ -1,3 +0,0 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/exec up -d

39
bin/script/update Executable file
View File

@ -0,0 +1,39 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PROJECT_DIR=$(realpath $SCRIPT_DIR/../../)
ENV_DIR=$(realpath $PROJECT_DIR/../../../)
SYSTEM_ENV_FILE="$PROJECT_DIR/.env"
CWD=$(pwd)
source $ENV_DIR/bin/messages
source $ENV_DIR/bin/drun
# Pull branch in project directory
infomsg "[backend]: Git pull"
cd "$PROJECT_DIR"
git pull
successmsg "[backend]: Git pull done"
# Install packages
infomsg '[backend] Composer install...'
drun backend composer install -o
successmsg '[backend] Composer install done'
# Dump autoload
infomsg "[backend]: Dump autoload"
drun backend composer da
successmsg "[backend]: Dump autoload done"
# Migrate Databases
infomsg '[backend] Running migrations...'
drun backend composer doctrine migrations:migrate --no-interaction
drun backend composer doctrine-log migrations:migrate --no-interaction
successmsg '[backend] Migrations done'
# Update permissions
infomsg "[backend]: Update roles and permissions"
drun backend composer console rbac:update
successmsg "[backend]: Update roles and permissions done"
# Switch back to current working directory
cd "$CWD"

View File

@ -21,11 +21,13 @@
"teewurst\/pipeline": "^3.0",
"guzzlehttp\/guzzle": "^7.8",
"micilini\/video-stream": "^1.0",
"nesbot\/carbon": "^3.0"
"nesbot\/carbon": "^3.0",
"ext-iconv": "*"
},
"autoload": {
"psr-4": {
"MyTube\\API\\Console\\": "src\/ApiDomain\/Console\/src",
"MyTube\\API\\External\\Analyze\\": "src\/ApiDomain\/External\/Analyze\/src",
"MyTube\\API\\External\\Authentication\\": "src\/ApiDomain\/External\/Authentication\/src",
"MyTube\\API\\External\\Health\\": "src\/ApiDomain\/External\/Health\/src",
"MyTube\\API\\External\\Tag\\": "src\/ApiDomain\/External\/Tag\/src",
@ -35,6 +37,7 @@
"MyTube\\API\\External\\VideoList\\": "src\/ApiDomain\/External\/VideoList\/src",
"MyTube\\Data\\Business\\": "src\/DataDomain\/Business\/src",
"MyTube\\Data\\Log\\": "src\/DataDomain\/Log\/src",
"MyTube\\Handling\\Analyze\\": "src\/HandlingDomain\/Analyze\/src",
"MyTube\\Handling\\Registration\\": "src\/HandlingDomain\/Registration\/src",
"MyTube\\Handling\\Role\\": "src\/HandlingDomain\/Role\/src",
"MyTube\\Handling\\Tag\\": "src\/HandlingDomain\/Tag\/src",
@ -78,8 +81,8 @@
"scripts": {
"dmg": "php bin\/doctrine-migrations.php migrations:generate",
"dmm": "php bin\/doctrine-migrations.php migrations:migrate --no-interaction",
"dmlg": "php bin\/doctrine-migrations-log.php migrations:generate",
"dmlm": "php bin\/doctrine-migrations-log.php migrations:migrate --no-interaction",
"doctrine": "php bin\/doctrine-migrations.php",
"doctrine-log": "php bin\/doctrine-migrations-log.php",
"console": "php bin\/console.php",
"createApi": "php bin\/createApi.php",
"createPipeline": "php bin\/createPipeline.php",
@ -98,6 +101,7 @@
},
"autoload-dev": {
"psr-4": {
"MyTube\\API\\External\\Analyze\\": "src\/ApiDomain\/External\/Analyze\/src",
"MyTube\\API\\External\\Authentication\\": "src\/ApiDomain\/External\/Authentication\/src",
"MyTube\\API\\External\\Health\\": "src\/ApiDomain\/External\/Health\/src",
"MyTube\\API\\External\\Tag\\": "src\/ApiDomain\/External\/Tag\/src",
@ -107,6 +111,7 @@
"MyTube\\API\\External\\VideoList\\": "src\/ApiDomain\/External\/VideoList\/src",
"MyTube\\Data\\Business\\": "src\/DataDomain\/Business\/src",
"MyTube\\Data\\Log\\": "src\/DataDomain\/Log\/src",
"MyTube\\Handling\\Analyze\\": "src\/HandlingDomain\/Analyze\/src",
"MyTube\\Handling\\Registration\\": "src\/HandlingDomain\/Registration\/src",
"MyTube\\Handling\\Role\\": "src\/HandlingDomain\/Role\/src",
"MyTube\\Handling\\Tag\\": "src\/HandlingDomain\/Tag\/src",

View File

@ -21,7 +21,8 @@
"teewurst/pipeline": "^3.0",
"guzzlehttp/guzzle": "^7.8",
"micilini/video-stream": "^1.0",
"nesbot/carbon": "^3.0"
"nesbot/carbon": "^3.0",
"ext-iconv": "*"
},
"autoload": {
"psr-4": {
@ -51,8 +52,8 @@
"scripts": {
"dmg": "php bin/doctrine-migrations.php migrations:generate",
"dmm": "php bin/doctrine-migrations.php migrations:migrate --no-interaction",
"dmlg": "php bin/doctrine-migrations-log.php migrations:generate",
"dmlm": "php bin/doctrine-migrations-log.php migrations:migrate --no-interaction",
"doctrine": "php bin/doctrine-migrations.php",
"doctrine-log": "php bin/doctrine-migrations-log.php",
"console": "php bin/console.php",
"createApi": "php bin/createApi.php",
"createPipeline": "php bin/createPipeline.php",

View File

@ -3,26 +3,9 @@
return [
'api' => [
'keys' => [
'myTube' => $_ENV['HOMEPAGE_API_KEY'],
'notification' => $_ENV['NOTIFICATION_API_KEY'],
],
'services' => [
'myTube' => [
'host' => 'myTube-backend-nginx',
'apis' => [
]
],
'notification' => [
'host' => 'notification-backend-nginx',
'apis' => [
'send-mail' => [
'path' => '/api/mail/send',
'method' => 'POST'
],
],
],
],
],
];

View File

@ -54,6 +54,7 @@ $aggregator = new ConfigAggregator([
\MyTube\Infrastructure\Session\ConfigProvider::class,
// HandlingDomain
\MyTube\Handling\Analyze\ConfigProvider::class,
\MyTube\Handling\User\ConfigProvider::class,
\MyTube\Handling\UserSession\ConfigProvider::class,
\MyTube\Handling\Registration\ConfigProvider::class,
@ -70,6 +71,7 @@ $aggregator = new ConfigAggregator([
\MyTube\API\External\Health\ConfigProvider::class,
\MyTube\API\External\User\ConfigProvider::class,
\MyTube\API\External\Authentication\ConfigProvider::class,
\MyTube\API\External\Analyze\ConfigProvider::class,
\MyTube\API\External\Video\ConfigProvider::class,
\MyTube\API\External\VideoList\ConfigProvider::class,
\MyTube\API\External\Tag\ConfigProvider::class,

View File

@ -1,51 +0,0 @@
version: '3'
networks:
myTube:
external: true
services:
myTube-backend-mysql:
image: myTube-backend-mysql
networks:
- myTube
build:
context: ./../
dockerfile: ./docker/mysql/dockerfile
volumes:
- /Users/flo/dev/backend/myTube/var/db:/var/lib/mysql:z
environment:
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
ports:
- 3306:3306
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
timeout: 20s
retries: 10
myTube-backend-app:
image: myTube-backend-app
networks:
- myTube
build:
context: ./../
dockerfile: ./docker/php/dockerfile
volumes:
- /Users/flo/dev/backend/myTube/:/var/www/html:z
ports:
- 9000:9000
depends_on:
myTube-backend-mysql:
condition: service_healthy
myTube-backend-nginx:
image: myTube-backend-nginx
networks:
- myTube
build:
context: ./../
dockerfile: ./docker/nginx/dockerfile
ports:
- 8080:80
depends_on:
- myTube-backend-app

View File

@ -1,4 +1,3 @@
version: '3'
networks:
mytube:
external: true
@ -45,7 +44,9 @@ services:
build:
context: ./../
dockerfile: ./docker/nginx/dockerfile
ports:
- 8080:80
labels:
- "traefik.http.routers.backend.rule=(Host(`mytube.srv`) || Host(`192.168.152.60`)) && PathPrefix(`/api`)"
- "traefik.http.routers.backend.entrypoints=websecure"
- "traefik.http.routers.backend.tls.certresolver=le"
depends_on:
- mytube-backend-app

View File

@ -1,4 +1,4 @@
CREATE DATABASE IF NOT EXISTS `log`;
CREATE DATABASE IF NOT EXISTS `myTube`;
CREATE DATABASE IF NOT EXISTS `mytube`;
GRANT ALL PRIVILEGES on *.* to 'myTube'@'%';
GRANT ALL PRIVILEGES on *.* to 'mytube'@'%';

View File

@ -1,5 +1,6 @@
<?php
use MyTube\API\Console\Command\AnalyzeTagsCommand;
use MyTube\API\Console\Command\AnalyzeVideoTitlesCommand;
use MyTube\API\Console\Command\InitializeDataCommand;
use MyTube\API\Console\Command\RbacUpdateCommand;
@ -11,5 +12,6 @@ return [
RbacUpdateCommand::class,
AnalyzeVideoTitlesCommand::class,
ReadUntaggedVideosCommand::class,
AnalyzeTagsCommand::class,
]
];

View File

@ -1,5 +1,6 @@
<?php
use MyTube\API\Console\Command\AnalyzeTagsCommand;
use MyTube\API\Console\Command\AnalyzeVideoTitlesCommand;
use MyTube\API\Console\Command\InitializeDataCommand;
use MyTube\API\Console\Command\RbacUpdateCommand;
@ -11,6 +12,7 @@ return [
InitializeDataCommand::class => AutoWiringFactory::class,
RbacUpdateCommand::class => AutoWiringFactory::class,
AnalyzeVideoTitlesCommand::class => AutoWiringFactory::class,
AnalyzeTagsCommand::class => AutoWiringFactory::class,
ReadUntaggedVideosCommand::class => AutoWiringFactory::class,
],
];

View File

@ -0,0 +1,133 @@
<?php
namespace MyTube\API\Console\Command;
use MyTube\Data\Business\Entity\Tag;
use MyTube\Data\Business\Entity\Video;
use MyTube\Data\Business\Manager\MyTubeEntityManager;
use MyTube\Data\Business\Repository\VideoRepository;
use MyTube\Handling\Tag\Rule\IsTagSubstringRule;
use MyTube\Handling\Video\Analyzer\VideoDurationAnalyzer;
use MyTube\Handling\Video\Analyzer\VideoTitleAnalyzer;
use MyTube\Infrastructure\Logging\Logger\Logger;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function Webmozart\Assert\Tests\StaticAnalysis\length;
#[AsCommand(name: 'analyze:tags', description: 'Analyzes video titles and add tags')]
class AnalyzeTagsCommand extends Command
{
private readonly VideoRepository $videoRepository;
public function __construct(
private readonly MyTubeEntityManager $entityManager,
private readonly IsTagSubstringRule $isTagSubstringRule,
private readonly Logger $logger,
) {
parent::__construct($this->getName());
$this->videoRepository = $this->entityManager->getRepository(Video::class);
}
protected function execute(
InputInterface $input,
OutputInterface $output
): int {
$io = new SymfonyStyle($input, $output);
try {
$videos = $this->videoRepository->findAll();
/** @var Video $video */
foreach ($videos as $video) {
$comments[] = $video->getTitle();
}
// Normalisierte Kommentare
$normalized_comments = array_map([$this, 'normalize'], $comments);
// Tokenisierung und Wortzählung
$word_counts = [];
foreach ($normalized_comments as $comment) {
$words = explode(' ', $comment);
foreach ($words as $word) {
if ($word) {
if (!isset($word_counts[$word])) {
$word_counts[$word] = 0;
}
$word_counts[$word]++;
}
}
}
// Konsolidierung der Wörter unter Berücksichtigung von Tippfehlern
$corrected_word_counts = [];
$dictionary = array_keys($word_counts);
foreach ($word_counts as $word => $count) {
$correct_word = $this->correct_typo($word, $dictionary);
if (!isset($corrected_word_counts[$correct_word])) {
$corrected_word_counts[$correct_word] = 0;
}
$corrected_word_counts[$correct_word] += $count;
}
// Sortieren nach Häufigkeit
arsort($corrected_word_counts);
$corrected_word_counts = array_reverse($corrected_word_counts);
// Ausgabe der häufigsten Wörter
foreach ($corrected_word_counts as $word => $count) {
if ($count > 3 && !$this->isTagSubstringRule->appliesTo($word)) {
echo $word . ": " . $count . "\n";
}
}
$io->success('OK!');
} catch (\Throwable $e) {
$io->error($e->getMessage());
$io->error($e->getTraceAsString());
$this->logger->error($e->getMessage(), ['exception' => $e]);
return Command::FAILURE;
}
return Command::SUCCESS;
}
function normalize($text) {
// Kleinbuchstaben
$text = mb_strtolower($text);
// Akzente entfernen
$text = iconv('UTF-8', 'ASCII//TRANSLIT', $text);
// Interpunktion entfernen
$text = preg_replace("/[^a-z\s]/", "", $text);
// Trimmen
$text = trim($text);
return $text;
}
// Tippfehlerkorrektur mit Levenshtein-Distanz
function correct_typo($word, $dictionary) {
$closest_word = $word;
$shortest_distance = -1;
foreach ($dictionary as $dict_word) {
$lev = levenshtein($word, $dict_word);
if ($lev == 0) {
$closest_word = $word;
$shortest_distance = 0;
break;
}
if ($lev <= 2 && ($lev < $shortest_distance || $shortest_distance < 0)) {
$closest_word = $dict_word;
$shortest_distance = $lev;
}
}
return $closest_word;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use MyTube\API\External\Analyze\Handler\AnalyzeVideosHandler;
use MyTube\API\External\Analyze\Handler\ReadVideoListHandler;
return [
[
'name' => 'analyze.analyze-videos',
'path' => '/api/analyze/analyze-videos[/]',
'allowed_methods' => ['POST'],
'middleware' => [
AnalyzeVideosHandler::class,
],
],
[
'name' => 'analyze.read-video-list',
'path' => '/api/analyze/read-video-list[/]',
'allowed_methods' => ['POST'],
'middleware' => [
ReadVideoListHandler::class,
],
],
];

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use MyTube\API\External\Analyze\Handler\AnalyzeVideosHandler;
use MyTube\API\External\Analyze\Handler\ReadVideoListHandler;
use MyTube\API\External\Analyze\ResponseFormatter\AnalyzeVideosResponseFormatter;
use MyTube\API\External\Analyze\ResponseFormatter\ReadVideoListResponseFormatter;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
return [
'factories' => [
// Handler
AnalyzeVideosHandler::class => AutoWiringFactory::class,
ReadVideoListHandler::class => AutoWiringFactory::class,
// Response Formatter
AnalyzeVideosResponseFormatter::class => AutoWiringFactory::class,
ReadVideoListResponseFormatter::class => AutoWiringFactory::class,
],
];

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace MyTube\API\External\Analyze;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => require __DIR__ . './../config/service_manager.php',
'routes' => require __DIR__ . '/./../config/routes.php',
];
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace MyTube\API\External\Analyze\Handler;
use MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos\AnalyzeVideosCommandHandler;
use MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos\AnalyzeVideosCommandBuilder;
use MyTube\API\External\Analyze\ResponseFormatter\AnalyzeVideosResponseFormatter;
use MyTube\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
use MyTube\Infrastructure\Response\SuccessResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class AnalyzeVideosHandler implements RequestHandlerInterface
{
public function __construct(
private readonly AnalyzeVideosCommandHandler $analyzeVideosCommandHandler,
private readonly AnalyzeVideosCommandBuilder $analyzeVideosCommandBuilder,
private readonly AnalyzeVideosResponseFormatter $responseFormatter,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$data = $request->getAttribute(AnalyzeBodyMiddleware::JSON_DATA);
$analyzeVideosCommand = $this->analyzeVideosCommandBuilder->build();
$result = $this->analyzeVideosCommandHandler->execute($analyzeVideosCommand);
return new SuccessResponse($this->responseFormatter->format($result));
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace MyTube\API\External\Analyze\Handler;
use MyTube\Handling\Analyze\Handler\Query\ReadVideoList\ReadVideoListQueryHandler;
use MyTube\Handling\Analyze\Handler\Query\ReadVideoList\ReadVideoListQueryBuilder;
use MyTube\API\External\Analyze\ResponseFormatter\ReadVideoListResponseFormatter;
use MyTube\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
use MyTube\Infrastructure\Response\SuccessResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ReadVideoListHandler implements RequestHandlerInterface
{
public function __construct(
private readonly ReadVideoListQueryHandler $readVideoListQueryHandler,
private readonly ReadVideoListQueryBuilder $readVideoListQueryBuilder,
private readonly ReadVideoListResponseFormatter $responseFormatter,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$data = $request->getAttribute(AnalyzeBodyMiddleware::JSON_DATA);
$readVideoListQuery = $this->readVideoListQueryBuilder->build(
$data['query'] ?? null,
$data['page'],
$data['perPage'],
$data['onlyTagless'] ?? false,
);
$result = $this->readVideoListQueryHandler->execute($readVideoListQuery);
return new SuccessResponse($this->responseFormatter->format($result));
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace MyTube\API\External\Analyze\ResponseFormatter;
use MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos\AnalyzeVideosCommandResult;
use MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos\Model\VideoChangedModel;
class AnalyzeVideosResponseFormatter
{
public function format(AnalyzeVideosCommandResult $analyzeVideosCommandResult): array
{
$result = [];
/** @var VideoChangedModel $changedVideo */
foreach ($analyzeVideosCommandResult->getChangedVideos() as $changedVideo) {
$tags = [];
$newTags = [];
$video = $changedVideo->getVideo();
foreach ($changedVideo->getNewTags() as $tag) {
$newTags[] = [
'id' => $tag->getId(),
'description' => $tag->getDescription(),
];
}
foreach ($video->getTags() as $tag) {
$tags[] = [
'id' => $tag->getId(),
'description' => $tag->getDescription(),
];
}
$result[] = [
'video' => [
'id' => $video->getId(),
'title' => $video->getTitle(),
'duration' => gmdate("H:i:s", $video->getDuration()),
'tags' => $tags,
],
'newTags' => $newTags,
'newDuration' => $changedVideo->isNewDuration()
];
}
return $result;
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace MyTube\API\External\Analyze\ResponseFormatter;
use MyTube\Data\Business\Entity\Tag;
use MyTube\Data\Business\Entity\Video;
use MyTube\Handling\Analyze\Handler\Query\ReadVideoList\ReadVideoListQueryResult;
class ReadVideoListResponseFormatter
{
public function format(ReadVideoListQueryResult $queryResult): array
{
$paginator = $queryResult->getPaginator();
$videos = $paginator->getIterator();
$items = [];
/** @var Video $video */
foreach ($videos as $video) {
$tags = [];
/** @var Tag $tag */
foreach ($video->getTags() as $tag) {
$tags[] = [
'id' => $tag->getId(),
'description' => $tag->getDescription()
];
}
$items[] = [
'title' => $video->getTitle(),
'id' => $video->getId(),
'duration' => $video->getDuration() === null ? 'n. def' : gmdate("H:i:s", $video->getDuration()),
'tags' => $tags
];
}
return [
'total' => $paginator->count(),
'items' => $items,
];
}
}

View File

@ -3,6 +3,7 @@
use MyTube\API\External\Tag\Handler\AddAliasHandler;
use MyTube\API\External\Tag\Handler\CreateHandler;
use MyTube\API\External\Tag\Handler\ReadDetailsHandler;
use MyTube\API\External\Tag\Handler\ReadThumbnailHandler;
use MyTube\API\External\Tag\Handler\ReadVideoListHandler;
return [
@ -38,4 +39,12 @@ return [
ReadVideoListHandler::class,
],
],
[
'name' => 'tag.thumbnail',
'path' => '/api/tag/thumbnail/:tagUuid[/]',
'allowed_methods' => ['GET'],
'middleware' => [
ReadThumbnailHandler::class,
],
],
];

View File

@ -3,6 +3,7 @@
use MyTube\API\External\Tag\Handler\AddAliasHandler;
use MyTube\API\External\Tag\Handler\CreateHandler;
use MyTube\API\External\Tag\Handler\ReadDetailsHandler;
use MyTube\API\External\Tag\Handler\ReadThumbnailHandler;
use MyTube\API\External\Tag\Handler\ReadVideoListHandler;
use MyTube\API\External\Tag\ResponseFormatter\AddAliasResponseFormatter;
use MyTube\API\External\Tag\ResponseFormatter\CreateResponseFormatter;
@ -17,6 +18,7 @@ return [
CreateHandler::class => AutoWiringFactory::class,
ReadDetailsHandler::class => AutoWiringFactory::class,
ReadVideoListHandler::class => AutoWiringFactory::class,
ReadThumbnailHandler::class => AutoWiringFactory::class,
// Response Formatter
AddAliasResponseFormatter::class => AutoWiringFactory::class,

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace MyTube\API\External\Tag\Handler;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\Response\TextResponse;
use MyTube\Handling\Tag\Handler\Query\ReadThumbnail\ReadThumbnailQueryBuilder;
use MyTube\Handling\Tag\Handler\Query\ReadThumbnail\ReadThumbnailQueryHandler;
use MyTube\Infrastructure\Exception\Exception\MyTubeException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Ramsey\Uuid\Uuid;
use Throwable;
class ReadThumbnailHandler implements RequestHandlerInterface
{
public function __construct(
private readonly ReadThumbnailQueryHandler $readThumbnailQueryHandler,
private readonly ReadThumbnailQueryBuilder $readThumbnailQueryBuilder,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$tagUuid = $request->getAttribute('tagUuid') ?? null;
if ($tagUuid === null) {
throw new MyTubeException('No tagUuid provided');
}
try {
$readThumbnailQuery = $this->readThumbnailQueryBuilder->build(
Uuid::fromString($tagUuid)
);
$result = $this->readThumbnailQueryHandler->execute($readThumbnailQuery);
$thumbnailVideo = $result->getThumbnailVideo();
if ($thumbnailVideo === null) {
throw new MyTubeException('No thumbnail video for tag');
}
$filePath = $thumbnailVideo->getDirectoryPath() . 'thumbnail.png';
if (!file_exists($filePath)) {
throw new MyTubeException('Thumbnail file does not exist');
}
$fileContent = file_get_contents($filePath);
$response = new TextResponse($fileContent);
return $response->withHeader('Content-Type', 'image/png');
} catch (Throwable $exception) {
return new JsonResponse('Not Found', 404);
}
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace MyTube\API\External\Tag\ResponseFormatter;
use MyTube\Handling\Tag\Handler\Query\ReadThumbnail\ReadThumbnailQueryResult;
class ReadThumbnailResponseFormatter
{
public function format(ReadThumbnailQueryResult $readThumbnailQueryResult): array
{
return [];
}
}

View File

@ -30,7 +30,6 @@ class ReadListHandler implements RequestHandlerInterface
$data['query'] ?? null,
$data['page'],
$data['perPage'],
$data['onlyTagless'] ?? false,
);
$result = $this->readListQueryHandler->execute($readListQuery);

View File

@ -9,16 +9,20 @@ class TagRepository extends EntityRepository {
?string $query,
): array
{
$queryBuilder = $this->createQueryBuilder('t');
$queryBuilder = $this->createQueryBuilder('t')
->leftJoin('t.videos', 'v')
->groupBy('t.id')
->select('t, COUNT(v.id) as HIDDEN video_count')
->orderBy('video_count', 'DESC');
if ($query !== null) {
$query = '%'.$query.'%';
$queryBuilder
->where('t.description like :query')
->where('t.description LIKE :query')
->setParameter('query', $query);
}
return $queryBuilder->getQuery()->execute();
return $queryBuilder->getQuery()->getResult();
}
}

View File

@ -18,7 +18,6 @@ class VideoRepository extends EntityRepository {
?string $query,
int $page,
int $perPage,
bool $onlyTagless,
?Tag $tag = null,
?string $orderBy = null,
string $orderDirection = 'desc'
@ -43,7 +42,7 @@ class VideoRepository extends EntityRepository {
->setParameter('tagId', $tag->getId(), UuidBinaryOrderedTimeType::NAME);
}
if ($onlyTagless) {
if (false) { //only tagless
$queryBuilder = $queryBuilder
->andWhere( $queryBuilder->expr()->isNull('t.id') );
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos\AnalyzeVideosCommandHandler;
use MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos\AnalyzeVideosCommandBuilder;
use MyTube\Handling\Analyze\Handler\Query\ReadVideoList\ReadVideoListQueryBuilder;
use MyTube\Handling\Analyze\Handler\Query\ReadVideoList\ReadVideoListQueryHandler;
use MyTube\Handling\Analyze\Repository\AnalyzeVideoRepository;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Reinfi\DependencyInjection\Factory\InjectionFactory;
return [
'factories' => [
/// Repository
AnalyzeVideoRepository::class => AutoWiringFactory::class,
/// CQRS
// AnalyzeVideos
AnalyzeVideosCommandBuilder::class => AutoWiringFactory::class,
AnalyzeVideosCommandHandler::class => AutoWiringFactory::class,
// Read Video List
ReadVideoListQueryBuilder::class => AutoWiringFactory::class,
ReadVideoListQueryHandler::class => AutoWiringFactory::class,
],
];

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => require __DIR__ . './../config/service_manager.php',
];
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos;
class AnalyzeVideosCommand
{
public function __construct(
#TODO
) {
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos;
class AnalyzeVideosCommandBuilder
{
public function build(
#TODO
): AnalyzeVideosCommand {
return new AnalyzeVideosCommand(
#TODO
);
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos;
use MyTube\Data\Business\Entity\Video;
use MyTube\Data\Business\Manager\MyTubeEntityManager;
use MyTube\Data\Business\Repository\VideoRepository;
use MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos\Model\VideoChangedModel;
use MyTube\Handling\Video\Analyzer\VideoDurationAnalyzer;
use MyTube\Handling\Video\Analyzer\VideoTitleAnalyzer;
class AnalyzeVideosCommandHandler
{
private readonly VideoRepository $videoRepository;
public function __construct(
private readonly MyTubeEntityManager $entityManager,
private readonly VideoTitleAnalyzer $titleAnalyzer,
private readonly VideoDurationAnalyzer $durationAnalyzer,
) {
$this->videoRepository = $this->entityManager->getRepository(Video::class);
}
public function execute(AnalyzeVideosCommand $analyzeVideosCommand): AnalyzeVideosCommandResult
{
$videos = $this->videoRepository->findAll();
$changedVideos = [];
/** @var Video $video */
foreach ($videos as $video) {
$newTags = $this->titleAnalyzer->analyze($video);
$newDuration = $this->durationAnalyzer->analyze($video);
if (count($newTags) > 0 || $newDuration) {
$changedVideos[] = new VideoChangedModel($video, $newTags, $newDuration);
$this->entityManager->persist($video);
$this->entityManager->flush();
}
}
return new AnalyzeVideosCommandResult($changedVideos);
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos;
class AnalyzeVideosCommandResult
{
public function __construct(
private array $changedVideos
) {
}
public function getChangedVideos(): array
{
return $this->changedVideos;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze\Handler\Command\AnalyzeVideos\Model;
use MyTube\Data\Business\Entity\Video;
class VideoChangedModel
{
public function __construct(
private readonly Video $video,
private readonly ?array $newTags = null,
private readonly bool $newDuration = false
) {
}
public function getVideo(): Video
{
return $this->video;
}
public function getNewTags(): ?array
{
return $this->newTags;
}
public function isNewDuration(): bool
{
return $this->newDuration;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze\Handler\Query\ReadVideoList;
class ReadVideoListQuery
{
public function __construct(
private readonly ?string $query,
private readonly int $page,
private readonly int $perPage,
private readonly bool $onlyTagless,
) {
}
public function getQuery(): ?string
{
return $this->query;
}
public function getPage(): int
{
return $this->page;
}
public function getPerPage(): int
{
return $this->perPage;
}
public function getOnlyTagless(): bool
{
return $this->onlyTagless;
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze\Handler\Query\ReadVideoList;
class ReadVideoListQueryBuilder
{
public function build(
?string $query,
int $page,
int $perPage,
bool $onlyTagless,
): ReadVideoListQuery {
return new ReadVideoListQuery(
$query,
$page,
$perPage,
$onlyTagless,
);
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze\Handler\Query\ReadVideoList;
use MyTube\Handling\Analyze\Repository\AnalyzeVideoRepository;
class ReadVideoListQueryHandler
{
public function __construct(
private readonly AnalyzeVideoRepository $analyzeVideoRepository
) {
}
public function execute(ReadVideoListQuery $readVideoListQuery): ReadVideoListQueryResult
{
return new ReadVideoListQueryResult(
$this->analyzeVideoRepository->findByFilter(
query: $readVideoListQuery->getQuery(),
page: $readVideoListQuery->getPage(),
perPage: $readVideoListQuery->getPerPage(),
onlyTagless: $readVideoListQuery->getOnlyTagless()
)
);
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze\Handler\Query\ReadVideoList;
use Doctrine\ORM\Tools\Pagination\Paginator;
class ReadVideoListQueryResult
{
public function __construct(
private readonly Paginator $paginator
) {
}
public function getPaginator(): Paginator
{
return $this->paginator;
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Analyze\Repository;
use Doctrine\ORM\Tools\Pagination\Paginator;
use MyTube\Data\Business\Entity\Video;
use MyTube\Data\Business\Manager\MyTubeEntityManager;
class AnalyzeVideoRepository
{
private const FIELD_MAP = [
'duration' => 'v.duration',
'title' => 'v.title',
'createdAt' => 'v.createdAt'
];
public function __construct(
private readonly MyTubeEntityManager $entityManager
) {
}
public function findByFilter(
?string $query,
int $page,
int $perPage,
bool $onlyTagless,
?string $orderBy = null,
string $orderDirection = 'asc'
): Paginator
{
$orderBy = self::FIELD_MAP[$orderBy] ?? self::FIELD_MAP['title'];
$queryBuilder = $this->entityManager->createQueryBuilder()
->select('v')
->from(Video::class, 'v')
->leftJoin('v.tags', 't');
if ($query !== null) {
$query = '%'.$query.'%';
$queryBuilder = $queryBuilder
->where('v.title like :query')
->orWhere('t.description like :query')
->setParameter('query', $query);
}
if ($onlyTagless) {
$queryBuilder = $queryBuilder
->andWhere( $queryBuilder->expr()->isNull('t.id') );
}
$queryBuilder->orderBy($orderBy,$orderDirection);
$queryBuilder->setFirstResult($perPage * ($page - 1));
$queryBuilder->setMaxResults($perPage);
return new Paginator($queryBuilder->getQuery());
}
}

View File

@ -8,8 +8,11 @@ use MyTube\Handling\Tag\Handler\Command\Create\CreateCommandBuilder;
use MyTube\Handling\Tag\Handler\Command\Create\CreateCommandHandler;
use MyTube\Handling\Tag\Handler\Query\ReadDetails\ReadDetailsQueryBuilder;
use MyTube\Handling\Tag\Handler\Query\ReadDetails\ReadDetailsQueryHandler;
use MyTube\Handling\Tag\Handler\Query\ReadThumbnail\ReadThumbnailQueryBuilder;
use MyTube\Handling\Tag\Handler\Query\ReadThumbnail\ReadThumbnailQueryHandler;
use MyTube\Handling\Tag\Handler\Query\ReadVideoList\ReadVideoListQueryBuilder;
use MyTube\Handling\Tag\Handler\Query\ReadVideoList\ReadVideoListQueryHandler;
use MyTube\Handling\Tag\Rule\IsTagSubstringRule;
use MyTube\Handling\Tag\Rule\TagAliasExistsRule;
use MyTube\Handling\Tag\Rule\TagExistsRule;
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
@ -22,6 +25,7 @@ return [
/// Rule
TagExistsRule::class => InjectionFactory::class,
TagAliasExistsRule::class => InjectionFactory::class,
IsTagSubstringRule::class => InjectionFactory::class,
/// Builder
TagBuilder::class => AutoWiringFactory::class,
@ -40,5 +44,8 @@ return [
// Read Video List
ReadVideoListQueryBuilder::class => AutoWiringFactory::class,
ReadVideoListQueryHandler::class => InjectionFactory::class,
// Read Thumbnail
ReadThumbnailQueryBuilder::class => AutoWiringFactory::class,
ReadThumbnailQueryHandler::class => InjectionFactory::class,
],
];

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Tag\Handler\Query\ReadThumbnail;
use Ramsey\Uuid\UuidInterface;
class ReadThumbnailQuery
{
public function __construct(
private readonly UuidInterface $tagUuid
) {
}
public function getTagUuid(): UuidInterface
{
return $this->tagUuid;
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Tag\Handler\Query\ReadThumbnail;
use Ramsey\Uuid\UuidInterface;
class ReadThumbnailQueryBuilder
{
public function build(
UuidInterface $tagUuid
): ReadThumbnailQuery {
return new ReadThumbnailQuery(
$tagUuid
);
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Tag\Handler\Query\ReadThumbnail;
use MyTube\Data\Business\Entity\Tag;
use MyTube\Data\Business\Repository\TagRepository;
use Reinfi\DependencyInjection\Annotation\InjectDoctrineRepository;
class ReadThumbnailQueryHandler
{
/**
* @InjectDoctrineRepository(
* entityManager="MyTube\Data\Business\Manager\MyTubeEntityManager",
* entity="MyTube\Data\Business\Entity\Tag"
* )
*/
public function __construct(
private readonly TagRepository $tagRepository,
) {
}
public function execute(ReadThumbnailQuery $readThumbnailQuery): ReadThumbnailQueryResult
{
$thumbnailVideo = null;
/** @var Tag $tag */
$tag = $this->tagRepository->findOneBy([
'id' => $readThumbnailQuery->getTagUuid()
]);
$videoCount = $tag->getVideos()->count();
if ($videoCount > 0) {
$thumbnailVideo = $tag->getVideos()[rand(0, $videoCount - 1)];
}
return new ReadThumbnailQueryResult($thumbnailVideo);
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace MyTube\Handling\Tag\Handler\Query\ReadThumbnail;
use MyTube\Data\Business\Entity\Video;
class ReadThumbnailQueryResult
{
public function __construct(
private readonly Video $thumbnailVideo
) {
}
public function getThumbnailVideo(): Video
{
return $this->thumbnailVideo;
}
}

View File

@ -44,7 +44,6 @@ class ReadVideoListQueryHandler
query: $readVideoListQuery->getQuery(),
page: $readVideoListQuery->getPage(),
perPage: $readVideoListQuery->getPerPage(),
onlyTagless: false,
tag: $tag
)
);

View File

@ -0,0 +1,34 @@
<?php
namespace MyTube\Handling\Tag\Rule;
use MyTube\Data\Business\Repository\TagRepository;
use Reinfi\DependencyInjection\Annotation\InjectDoctrineRepository;
class IsTagSubstringRule
{
/**
* @InjectDoctrineRepository(
* entityManager="MyTube\Data\Business\Manager\MyTubeEntityManager",
* entity="MyTube\Data\Business\Entity\Tag"
* )
*/
public function __construct(
private readonly TagRepository $tagRepository,
) {
}
public function appliesTo(
string $substring,
): bool
{
$substring = "%" . $substring . "%";
$qb = $this->tagRepository->createQueryBuilder('t')
->where('t.description like :substring')
->setParameter('substring', $substring);
return count($qb->getQuery()->getResult()) !== 0;
}
}

View File

@ -15,7 +15,7 @@ class VideoDurationAnalyzer
{
public function analyze(
Video $video
): void {
): bool {
if ($video->getDuration() === null) {
$command = sprintf(
"ffprobe -i %s%s -show_entries format=duration -v quiet -of csv='p=0'",
@ -26,7 +26,9 @@ class VideoDurationAnalyzer
$duration = shell_exec($command);
if ($duration !== null) {
$video->setDuration(intval($duration));
return true;
}
}
return false;
}
}

View File

@ -31,6 +31,7 @@ class VideoTitleAnalyzer
$tags = $this->tagRepository->findAll();
$matches = [];
$appliedMatches = [];
/** @var Tag $tag */
foreach ($tags as $tag) {
@ -50,10 +51,13 @@ class VideoTitleAnalyzer
/** @var Tag $match */
foreach ($matches as $match) {
if(!$video->getTags()->contains($match)) {
$appliedMatches[] = $match;
$video->addTag($match);
}
}
return $matches;
return $appliedMatches;
}
private function normalizeString(string $string): string {

View File

@ -9,8 +9,7 @@ class ReadListQuery
public function __construct(
private readonly ?string $query,
private readonly int $page,
private readonly int $perPage,
private readonly bool $onlyTagless,
private readonly int $perPage
) {
}
@ -28,9 +27,4 @@ class ReadListQuery
{
return $this->perPage;
}
public function getOnlyTagless(): bool
{
return $this->onlyTagless;
}
}

View File

@ -10,13 +10,11 @@ class ReadListQueryBuilder
?string $query,
int $page,
int $perPage,
bool $onlyTagless,
): ReadListQuery {
return new ReadListQuery(
$query,
$page,
$perPage,
$onlyTagless,
);
}
}

View File

@ -26,8 +26,7 @@ class ReadListQueryHandler
$this->videoRepository->findByFilter(
query: $readListQuery->getQuery(),
page: $readListQuery->getPage(),
perPage: $readListQuery->getPerPage(),
onlyTagless: $readListQuery->getOnlyTagless()
perPage: $readListQuery->getPerPage()
)
);
}