Support multiple cuts, for removing of ads

This commit is contained in:
Daniel Siepmann 2020-09-30 13:48:25 +02:00
parent 86a7dbef19
commit 8b8f7fcab1
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
5 changed files with 254 additions and 74 deletions

View file

@ -40,8 +40,8 @@ class Command
$videoInfo = new VideoInfo( $videoInfo = new VideoInfo(
$input->getArgument('file'), $input->getArgument('file'),
$input->getArgument('start'), $input->getArgument('start'),
$input->getArgument('end') $input->getArgument('end'),
// $input->getOption('ad') $input->getOption('ad')
); );
$cutting = new Cutting($videoInfo); $cutting = new Cutting($videoInfo);

View file

@ -42,17 +42,24 @@ class Cutting
public function getCommandsForCutting(): \Generator public function getCommandsForCutting(): \Generator
{ {
$command = sprintf( $start = $this->video->getStart();
'ffmpeg -y -ss %s -to %s -i %s -c copy %s', $end = $this->video->getEnd();
escapeshellarg($this->video->getStart()),
escapeshellarg($this->video->getEnd()),
escapeshellarg($this->video->getOriginalFilename()),
escapeshellarg($this->getNextTempFilename())
);
// 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 public function getCommandForGeneratingMetadata(): Process
@ -75,16 +82,12 @@ class Cutting
); );
} }
$command = sprintf( $lines = [];
'echo "file %s" > %s', foreach ($this->tempFilenames as $filename) {
escapeshellarg($this->tempFilenames[0]), $lines[] = 'file ' . $filename;
escapeshellarg($this->getConcatFilename()) }
);
// if $ads ; then
// echo "file '$result2'" >> $resultTxt
// fi
$command = 'printf ' . escapeshellarg(implode('\n', $lines)) . ' > ' . $this->getConcatFilename();
return Process::fromShellCommandline($command); return Process::fromShellCommandline($command);
} }
@ -117,6 +120,19 @@ class Cutting
return Process::fromShellCommandline($command); 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 private function getNextTempFilename(): string
{ {
$count = count($this->tempFilenames) + 1; $count = count($this->tempFilenames) + 1;

View file

@ -26,15 +26,18 @@ class VideoInfo
private $filename = ''; private $filename = '';
private $start = ''; private $start = '';
private $end = ''; private $end = '';
private $ads = [];
public function __construct( public function __construct(
string $filename, string $filename,
string $start, string $start,
string $end string $end,
array $ads
) { ) {
$this->filename = $filename; $this->filename = $filename;
$this->start = $start; $this->start = $start;
$this->end = $end; $this->end = $end;
$this->ads = $this->getParsedAds($ads);
} }
public function getStart(): string public function getStart(): string
@ -47,53 +50,9 @@ class VideoInfo
return $this->end; return $this->end;
} }
public function getSeries(): string public function getAds(): array
{ {
return $this->getFilenameParts()[0]; return $this->ads;
}
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);
} }
public function getTitle(): string 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 private function getExtension(): string
{ {
$file = new \SplFileInfo($this->filename); $file = new \SplFileInfo($this->filename);

View file

@ -50,11 +50,12 @@ class CuttingTest extends TestCase
/** /**
* @test * @test
*/ */
public function generatesCommandsForCutting(): void public function generatesCommandsForCuttingWithoutAds(): void
{ {
$video = $this->prophesize(VideoInfo::class); $video = $this->prophesize(VideoInfo::class);
$video->getStart()->willReturn('10.05.20'); $video->getStart()->willReturn('10.05.20');
$video->getEnd()->willReturn('13.10.25'); $video->getEnd()->willReturn('13.10.25');
$video->getAds()->willReturn([]);
$video->getOriginalFilename()->willReturn('Some-Video.mp4'); $video->getOriginalFilename()->willReturn('Some-Video.mp4');
$video->getOriginalFilenameWithSuffix('cut-1')->willReturn('Some-Video-cut-1.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 * @test
*/ */
@ -116,11 +158,12 @@ class CuttingTest extends TestCase
/** /**
* @test * @test
*/ */
public function generatesCommandsForGeneratingConcatInputFile(): void public function generatesCommandsForGeneratingConcatInputFileWithNoAds(): void
{ {
$video = $this->prophesize(VideoInfo::class); $video = $this->prophesize(VideoInfo::class);
$video->getStart()->willReturn('10.05.20'); $video->getStart()->willReturn('10.05.20');
$video->getEnd()->willReturn('13.10.25'); $video->getEnd()->willReturn('13.10.25');
$video->getAds()->willReturn([]);
$video->getOriginalFilename()->willReturn('Some-Video.mp4'); $video->getOriginalFilename()->willReturn('Some-Video.mp4');
$video->getOriginalFilenameWithSuffix('cut-1')->willReturn('Some-Video-cut-1.mp4'); $video->getOriginalFilenameWithSuffix('cut-1')->willReturn('Some-Video-cut-1.mp4');
$video->getOriginalFilenameWithSuffix('concat')->willReturn('Some-Video-concat.mp4'); $video->getOriginalFilenameWithSuffix('concat')->willReturn('Some-Video-concat.mp4');
@ -136,7 +179,42 @@ class CuttingTest extends TestCase
$command = $subject->getCommandForGeneratingConcatInputFile(); $command = $subject->getCommandForGeneratingConcatInputFile();
static::assertSame( 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() $command->getCommandLine()
); );
} }
@ -196,6 +274,7 @@ class CuttingTest extends TestCase
$video = $this->prophesize(VideoInfo::class); $video = $this->prophesize(VideoInfo::class);
$video->getStart()->willReturn('10.05.20'); $video->getStart()->willReturn('10.05.20');
$video->getEnd()->willReturn('13.10.25'); $video->getEnd()->willReturn('13.10.25');
$video->getAds()->willReturn([]);
$video->getOriginalFilename()->willReturn('Some-Video.mp4'); $video->getOriginalFilename()->willReturn('Some-Video.mp4');
$video->getTargetFilePath()->willReturn('Series-Name/Series-01/10-episode title.mp4'); $video->getTargetFilePath()->willReturn('Series-Name/Series-01/10-episode title.mp4');
$video->getOriginalFilenameWithSuffix('cut-1')->willReturn('Some-Video-cut-1.mp4'); $video->getOriginalFilenameWithSuffix('cut-1')->willReturn('Some-Video-cut-1.mp4');

View file

@ -37,12 +37,79 @@ class VideoInfoTest extends TestCase
$subject = new VideoInfo( $subject = new VideoInfo(
'Example-Video-File.mp4', 'Example-Video-File.mp4',
'05:20.1', '05:20.1',
'32:30.30' '32:30.30',
[]
); );
static::assertInstanceOf(VideoInfo::class, $subject); 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 * @test
* @dataProvider possibleFilenames * @dataProvider possibleFilenames
@ -54,7 +121,8 @@ class VideoInfoTest extends TestCase
$subject = new VideoInfo( $subject = new VideoInfo(
$filename, $filename,
'05:20.1', '05:20.1',
'32:30.30' '32:30.30',
[]
); );
static::assertSame( static::assertSame(
@ -71,7 +139,8 @@ class VideoInfoTest extends TestCase
$subject = new VideoInfo( $subject = new VideoInfo(
'Example-Video-File.mp4', 'Example-Video-File.mp4',
'05:20.1', '05:20.1',
'32:30.30' '32:30.30',
[]
); );
static::assertSame( static::assertSame(
@ -88,7 +157,8 @@ class VideoInfoTest extends TestCase
$subject = new VideoInfo( $subject = new VideoInfo(
'Example-Video-File.mp4', 'Example-Video-File.mp4',
'05:20.1', '05:20.1',
'32:30.30' '32:30.30',
[]
); );
static::assertSame( static::assertSame(