From 8b8f7fcab12966ac45ef37944289f2d8bef2eedd Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Wed, 30 Sep 2020 13:48:25 +0200 Subject: [PATCH] Support multiple cuts, for removing of ads --- src/Command.php | 4 +- src/Cutting.php | 52 +++++++++++------ src/VideoInfo.php | 109 ++++++++++++++++++++--------------- tests/Unit/CuttingTest.php | 85 ++++++++++++++++++++++++++- tests/Unit/VideoInfoTest.php | 78 +++++++++++++++++++++++-- 5 files changed, 254 insertions(+), 74 deletions(-) diff --git a/src/Command.php b/src/Command.php index 4fa29cb..a494ee8 100644 --- a/src/Command.php +++ b/src/Command.php @@ -40,8 +40,8 @@ class Command $videoInfo = new VideoInfo( $input->getArgument('file'), $input->getArgument('start'), - $input->getArgument('end') - // $input->getOption('ad') + $input->getArgument('end'), + $input->getOption('ad') ); $cutting = new Cutting($videoInfo); diff --git a/src/Cutting.php b/src/Cutting.php index 5095886..ba70263 100644 --- a/src/Cutting.php +++ b/src/Cutting.php @@ -42,17 +42,24 @@ class Cutting public function getCommandsForCutting(): \Generator { - $command = sprintf( - 'ffmpeg -y -ss %s -to %s -i %s -c copy %s', - escapeshellarg($this->video->getStart()), - escapeshellarg($this->video->getEnd()), - escapeshellarg($this->video->getOriginalFilename()), - escapeshellarg($this->getNextTempFilename()) - ); + $start = $this->video->getStart(); + $end = $this->video->getEnd(); - // TODO: Generate multiple once ads are in + if ($this->video->getAds() === []) { + yield $this->getCuttingCommand($start, $end); + return; + } - yield Process::fromShellCommandline($command); + // Each video starts with end of last ad, and end at next ad start. + foreach ($this->video->getAds() as [$adStart, $adEnd]) { + $end = $adStart; + + yield $this->getCuttingCommand($start, $end); + + $start = $adEnd; + } + + yield $this->getCuttingCommand($start, $this->video->getEnd()); } public function getCommandForGeneratingMetadata(): Process @@ -75,16 +82,12 @@ class Cutting ); } - $command = sprintf( - 'echo "file %s" > %s', - escapeshellarg($this->tempFilenames[0]), - escapeshellarg($this->getConcatFilename()) - ); - - // if $ads ; then - // echo "file '$result2'" >> $resultTxt - // fi + $lines = []; + foreach ($this->tempFilenames as $filename) { + $lines[] = 'file ' . $filename; + } + $command = 'printf ' . escapeshellarg(implode('\n', $lines)) . ' > ' . $this->getConcatFilename(); return Process::fromShellCommandline($command); } @@ -117,6 +120,19 @@ class Cutting return Process::fromShellCommandline($command); } + private function getCuttingCommand(string $start, string $end): Process + { + $command = sprintf( + 'ffmpeg -y -ss %s -to %s -i %s -c copy %s', + escapeshellarg($start), + escapeshellarg($end), + escapeshellarg($this->video->getOriginalFilename()), + escapeshellarg($this->getNextTempFilename()) + ); + + return Process::fromShellCommandline($command); + } + private function getNextTempFilename(): string { $count = count($this->tempFilenames) + 1; diff --git a/src/VideoInfo.php b/src/VideoInfo.php index 9000aa1..dcba19c 100644 --- a/src/VideoInfo.php +++ b/src/VideoInfo.php @@ -26,15 +26,18 @@ class VideoInfo private $filename = ''; private $start = ''; private $end = ''; + private $ads = []; public function __construct( string $filename, string $start, - string $end + string $end, + array $ads ) { $this->filename = $filename; $this->start = $start; $this->end = $end; + $this->ads = $this->getParsedAds($ads); } public function getStart(): string @@ -47,53 +50,9 @@ class VideoInfo return $this->end; } - public function getSeries(): string + public function getAds(): array { - return $this->getFilenameParts()[0]; - } - - public function hasSeason(): bool - { - foreach ($this->getFilenameParts() as $part) { - if (stripos($part, 'season-') === 0) { - return true; - } - } - - return false; - } - - public function getSeasonNumber(): int - { - foreach ($this->getFilenameParts() as $part) { - if (stripos($part, 'season-') === 0) { - return (int) str_replace('season-', '', $part); - } - } - - throw new \Exception('No season number detected.', 1601458223); - } - - public function hasEpisode(): bool - { - foreach ($this->getFilenameParts() as $part) { - if (stripos($part, 'episode-') === 0) { - return true; - } - } - - return false; - } - - public function getEpisodeNumber(): int - { - foreach ($this->getFilenameParts() as $part) { - if (stripos($part, 'episode-') === 0) { - return (int) str_replace('episode-', '', $part); - } - } - - throw new \Exception('No episode number detected.', 1601458223); + return $this->ads; } public function getTitle(): string @@ -140,6 +99,62 @@ class VideoInfo ); } + private function getParsedAds(array $ads): array + { + return array_map(function (string $ad) { + return explode('/', $ad, 2); + }, $ads); + } + + private function getSeries(): string + { + return $this->getFilenameParts()[0]; + } + + private function hasSeason(): bool + { + foreach ($this->getFilenameParts() as $part) { + if (stripos($part, 'season-') === 0) { + return true; + } + } + + return false; + } + + private function getSeasonNumber(): int + { + foreach ($this->getFilenameParts() as $part) { + if (stripos($part, 'season-') === 0) { + return (int) str_replace('season-', '', $part); + } + } + + throw new \Exception('No season number detected.', 1601458223); + } + + private function hasEpisode(): bool + { + foreach ($this->getFilenameParts() as $part) { + if (stripos($part, 'episode-') === 0) { + return true; + } + } + + return false; + } + + private function getEpisodeNumber(): int + { + foreach ($this->getFilenameParts() as $part) { + if (stripos($part, 'episode-') === 0) { + return (int) str_replace('episode-', '', $part); + } + } + + throw new \Exception('No episode number detected.', 1601458223); + } + private function getExtension(): string { $file = new \SplFileInfo($this->filename); diff --git a/tests/Unit/CuttingTest.php b/tests/Unit/CuttingTest.php index 20c79e2..8a85605 100644 --- a/tests/Unit/CuttingTest.php +++ b/tests/Unit/CuttingTest.php @@ -50,11 +50,12 @@ class CuttingTest extends TestCase /** * @test */ - public function generatesCommandsForCutting(): void + public function generatesCommandsForCuttingWithoutAds(): void { $video = $this->prophesize(VideoInfo::class); $video->getStart()->willReturn('10.05.20'); $video->getEnd()->willReturn('13.10.25'); + $video->getAds()->willReturn([]); $video->getOriginalFilename()->willReturn('Some-Video.mp4'); $video->getOriginalFilenameWithSuffix('cut-1')->willReturn('Some-Video-cut-1.mp4'); @@ -74,6 +75,47 @@ class CuttingTest extends TestCase ); } + /** + * @test + */ + public function generatesCommandsForCuttingWithAds(): void + { + $video = $this->prophesize(VideoInfo::class); + $video->getStart()->willReturn('1:05.20'); + $video->getEnd()->willReturn('13:10.25'); + $video->getAds()->willReturn([ + ['2:10', '3:05'], + ['10:07', '11:10'], + ]); + $video->getOriginalFilename()->willReturn('Some-Video.mp4'); + $video->getOriginalFilenameWithSuffix('cut-1')->willReturn('Some-Video-cut-1.mp4'); + $video->getOriginalFilenameWithSuffix('cut-2')->willReturn('Some-Video-cut-2.mp4'); + $video->getOriginalFilenameWithSuffix('cut-3')->willReturn('Some-Video-cut-3.mp4'); + + $subject = new Cutting( + $video->reveal() + ); + + $commands = []; + foreach ($subject->getCommandsForCutting() as $command) { + $commands[] = $command; + } + + static::assertCount(3, $commands, 'Did not get expected number of cutting commands'); + static::assertSame( + "ffmpeg -y -ss '1:05.20' -to '2:10' -i 'Some-Video.mp4' -c copy '/tmp/Some-Video-cut-1.mp4'", + $commands[0]->getCommandLine() + ); + static::assertSame( + "ffmpeg -y -ss '3:05' -to '10:07' -i 'Some-Video.mp4' -c copy '/tmp/Some-Video-cut-2.mp4'", + $commands[1]->getCommandLine() + ); + static::assertSame( + "ffmpeg -y -ss '11:10' -to '13:10.25' -i 'Some-Video.mp4' -c copy '/tmp/Some-Video-cut-3.mp4'", + $commands[2]->getCommandLine() + ); + } + /** * @test */ @@ -116,11 +158,12 @@ class CuttingTest extends TestCase /** * @test */ - public function generatesCommandsForGeneratingConcatInputFile(): void + public function generatesCommandsForGeneratingConcatInputFileWithNoAds(): void { $video = $this->prophesize(VideoInfo::class); $video->getStart()->willReturn('10.05.20'); $video->getEnd()->willReturn('13.10.25'); + $video->getAds()->willReturn([]); $video->getOriginalFilename()->willReturn('Some-Video.mp4'); $video->getOriginalFilenameWithSuffix('cut-1')->willReturn('Some-Video-cut-1.mp4'); $video->getOriginalFilenameWithSuffix('concat')->willReturn('Some-Video-concat.mp4'); @@ -136,7 +179,42 @@ class CuttingTest extends TestCase $command = $subject->getCommandForGeneratingConcatInputFile(); static::assertSame( - 'echo "file \'/tmp/Some-Video-cut-1.mp4\'" > \'/tmp/Some-Video-concat.txt\'', + "printf 'file /tmp/Some-Video-cut-1.mp4' > /tmp/Some-Video-concat.txt", + $command->getCommandLine() + ); + } + + /** + * @test + */ + public function generatesCommandsForGeneratingConcatInputFileWithAds(): void + { + $video = $this->prophesize(VideoInfo::class); + $video->getStart()->willReturn('10.05.20'); + $video->getEnd()->willReturn('13.10.25'); + $video->getAds()->willReturn([ + ['2:10', '3:05'], + ['10:07', '11:10'], + ]); + $video->getOriginalFilename()->willReturn('Some-Video.mp4'); + $video->getOriginalFilenameWithSuffix('cut-1')->willReturn('Some-Video-cut-1.mp4'); + $video->getOriginalFilenameWithSuffix('cut-2')->willReturn('Some-Video-cut-2.mp4'); + $video->getOriginalFilenameWithSuffix('cut-3')->willReturn('Some-Video-cut-3.mp4'); + $video->getOriginalFilenameWithSuffix('concat')->willReturn('Some-Video-concat.mp4'); + + $subject = new Cutting( + $video->reveal() + ); + + foreach ($subject->getCommandsForCutting() as $command) { + // Just generate all commands to have proper internal state + } + + $command = $subject->getCommandForGeneratingConcatInputFile(); + + static::assertSame( + 'printf \'file /tmp/Some-Video-cut-1.mp4\nfile /tmp/Some-Video-cut-2.mp4\nfile /tmp/Some-Video-cut-3.mp4\'' + . ' > /tmp/Some-Video-concat.txt', $command->getCommandLine() ); } @@ -196,6 +274,7 @@ class CuttingTest extends TestCase $video = $this->prophesize(VideoInfo::class); $video->getStart()->willReturn('10.05.20'); $video->getEnd()->willReturn('13.10.25'); + $video->getAds()->willReturn([]); $video->getOriginalFilename()->willReturn('Some-Video.mp4'); $video->getTargetFilePath()->willReturn('Series-Name/Series-01/10-episode title.mp4'); $video->getOriginalFilenameWithSuffix('cut-1')->willReturn('Some-Video-cut-1.mp4'); diff --git a/tests/Unit/VideoInfoTest.php b/tests/Unit/VideoInfoTest.php index a25bca8..ba46123 100644 --- a/tests/Unit/VideoInfoTest.php +++ b/tests/Unit/VideoInfoTest.php @@ -37,12 +37,79 @@ class VideoInfoTest extends TestCase $subject = new VideoInfo( 'Example-Video-File.mp4', '05:20.1', - '32:30.30' + '32:30.30', + [] ); static::assertInstanceOf(VideoInfo::class, $subject); } + /** + * @test + */ + public function getInitialSetStart() + { + $subject = new VideoInfo( + 'Example-Video-File.mp4', + '05:20.1', + '32:30.30', + [] + ); + + static::assertSame('05:20.1', $subject->getStart()); + } + + /** + * @test + */ + public function getInitialSetEnd() + { + $subject = new VideoInfo( + 'Example-Video-File.mp4', + '05:20.1', + '32:30.30', + [] + ); + + static::assertSame('32:30.30', $subject->getEnd()); + } + + /** + * @test + */ + public function getInitialEmptySetAds() + { + $subject = new VideoInfo( + 'Example-Video-File.mp4', + '05:20.1', + '32:30.30', + [] + ); + + static::assertSame([], $subject->getAds()); + } + + /** + * @test + */ + public function getInitialSetAds() + { + $subject = new VideoInfo( + 'Example-Video-File.mp4', + '05:20.1', + '32:30.30', + [ + '10:35/15:10', + '21:38/31:02', + ] + ); + + static::assertSame([ + ['10:35', '15:10'], + ['21:38', '31:02'], + ], $subject->getAds()); + } + /** * @test * @dataProvider possibleFilenames @@ -54,7 +121,8 @@ class VideoInfoTest extends TestCase $subject = new VideoInfo( $filename, '05:20.1', - '32:30.30' + '32:30.30', + [] ); static::assertSame( @@ -71,7 +139,8 @@ class VideoInfoTest extends TestCase $subject = new VideoInfo( 'Example-Video-File.mp4', '05:20.1', - '32:30.30' + '32:30.30', + [] ); static::assertSame( @@ -88,7 +157,8 @@ class VideoInfoTest extends TestCase $subject = new VideoInfo( 'Example-Video-File.mp4', '05:20.1', - '32:30.30' + '32:30.30', + [] ); static::assertSame(