Support multiple cuts, for removing of ads
This commit is contained in:
parent
86a7dbef19
commit
8b8f7fcab1
5 changed files with 254 additions and 74 deletions
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue