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(
|
||||
$input->getArgument('file'),
|
||||
$input->getArgument('start'),
|
||||
$input->getArgument('end')
|
||||
// $input->getOption('ad')
|
||||
$input->getArgument('end'),
|
||||
$input->getOption('ad')
|
||||
);
|
||||
|
||||
$cutting = new Cutting($videoInfo);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue