Skip to content

Instantly share code, notes, and snippets.

@eexit
Last active March 14, 2022 17:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eexit/f43dedd29489d166a2b338ed31be7780 to your computer and use it in GitHub Desktop.
Save eexit/f43dedd29489d166a2b338ed31be7780 to your computer and use it in GitHub Desktop.
YouTube Watch History Replayer

YouTube Watch History Replayer

Replay YouTube Watch History from Google Takeout. Relies on https://github.com/yt-dlp/yt-dlp.

Usage

Go to https://www.youtube.com/ then save the cookie export using Get cookies.txt.

Do a YouTube Takeout and download the archive. In the archive, there's a file called watch-history.json.

composer install
chmod +x youtube-watch-history-replayer
./youtube-watch-history-replayer --watch-history-file=./path/to/watch-history.json --cookies-file=./path/youtube.com_cookies.txt

YouTube Watch History Replayer
==============================

 Will now replay 4523 URLs:
    805/4523 [=====>-----------------------]   18% 26 min
{
"require": {
"symfony/process": "^6.0",
"symfony/console": "^6.0"
}
}
#!/usr/bin/env php
<?php
declare(strict_types=1);
require_once __DIR__.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php';
use Symfony\Component\Process\Process;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SingleCommandApplication;
use Symfony\Component\Console\Style\SymfonyStyle;
(new SingleCommandApplication())
->setVersion('0.1.0')
->addOption('watch-history-file', null, InputOption::VALUE_REQUIRED, 'Path to the watch-history.json file', '.'.DIRECTORY_SEPARATOR.'watch-history.json')
->addOption('cookies-file', null, InputOption::VALUE_REQUIRED, 'Path to the youtube.com_cookies.txt file', '.'.DIRECTORY_SEPARATOR.'youtube.com_cookies.txt')
->setCode(function (InputInterface $input, OutputInterface $output) {
$io = new SymfonyStyle($input, $output);
$io->title('YouTube Watch History Replay');
if (file_exists($input->getOption('watch-history-file').'.tmp')) {
$ids = json_decode(file_get_contents($input->getOption('watch-history-file').'.tmp'), associative: true);
} else {
$ids = [];
$history = array_reverse(
json_decode(
file_get_contents($input->getOption('watch-history-file')),
associative: true
)
);
foreach ($history as $entry) {
if (isset($entry['titleUrl']) && strtolower($entry['header']) === 'youtube') {
$qs = [];
// Parses the URL to get the ID
parse_str(parse_url($entry['titleUrl'], PHP_URL_QUERY), $qs);
$ids[] = $qs['v'] ?? null;
}
}
$ids = array_values(array_unique(array_filter($ids)));
file_put_contents($input->getOption('watch-history-file').'.tmp', json_encode($ids));
}
$count = \count($ids);
if (false === $ids || 0 === $count) {
$io->error('Empty or not found watch-history-file');
return Command::INVALID;
}
if (!file_exists($input->getOption('cookies-file'))) {
$io->error('cookies-file not found');
return Command::INVALID;
}
$io->text('Will now replay '.$count.' URLs:');
$progressBar = new ProgressBar($output);
$progressBar->setFormat('verbose');
$progressBar->start($count);
$ytdlp = (new ExecutableFinder())->find('yt-dlp');
$cookiesFile = $input->getOption('cookies-file');
// This is the yt-dlp command, you can add/remove options as needed
$cmdTemplate = <<<TPL
%s
--simulate
--mark-watched
--abort-on-error
--no-playlist
--ignore-config
--no-warnings
--no-progress
--cookies "%s"
"https://www.youtube.com/watch?v=%s"
TPL;
$errors = [];
foreach ($ids as $id) {
try {
$cmd = str_replace(
PHP_EOL,
' ',
sprintf(
$cmdTemplate,
$ytdlp,
$cookiesFile,
$id
)
);
(Process::fromShellCommandline($cmd))->mustRun();
} catch (\Throwable $e) {
$errors[$id] = $e->getMessage();
} finally {
$progressBar->advance();
}
}
$progressBar->finish();
file_put_contents('errors.log.json', json_encode($errors));
$io->newLine(2);
\count($errors) ? $io->warning('done with errors') : $io->success('done');
@unlink($input->getOption('watch-history-file').'.tmp');
return Command::SUCCESS;
})
->run();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment