Skip to content

Commit 516c8b5

Browse files
author
pablo.garcia
committed
Add new Youtube commands
1 parent 8516524 commit 516c8b5

File tree

88 files changed

+12199
-28
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+12199
-28
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pumukit\YoutubeBundle\Application\Command\Account;
6+
7+
use Doctrine\ODM\MongoDB\DocumentManager;
8+
use Pumukit\YoutubeBundle\Domain\Model\YoutubeAccount;
9+
use Pumukit\YoutubeBundle\Infrastructure\Service\GoogleClientFactory;
10+
use Symfony\Component\Console\Attribute\AsCommand;
11+
use Symfony\Component\Console\Command\Command;
12+
use Symfony\Component\Console\Input\InputArgument;
13+
use Symfony\Component\Console\Input\InputInterface;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
use Symfony\Component\Console\Style\SymfonyStyle;
16+
17+
#[AsCommand(
18+
name: 'youtube:account:authorize',
19+
description: 'Generate OAuth authorization URL for YouTube account'
20+
)]
21+
class AuthorizeAccountCommand extends Command
22+
{
23+
public function __construct(
24+
private readonly GoogleClientFactory $clientFactory,
25+
private readonly DocumentManager $documentManager
26+
) {
27+
parent::__construct();
28+
}
29+
30+
protected function configure(): void
31+
{
32+
$this
33+
->addArgument('account-name', InputArgument::REQUIRED, 'YouTube account name')
34+
->setHelp(<<<'EOF'
35+
The <info>%command.name%</info> command generates an OAuth authorization URL:
36+
37+
<info>php %command.full_name% my-account</info>
38+
39+
This will generate a URL that you need to visit in your browser to authorize the application.
40+
After authorization, you'll receive a code that you need to save using youtube:account:token.
41+
EOF
42+
);
43+
}
44+
45+
protected function execute(InputInterface $input, OutputInterface $output): int
46+
{
47+
$io = new SymfonyStyle($input, $output);
48+
$accountName = $input->getArgument('account-name');
49+
50+
// Find account
51+
$account = $this->documentManager->getRepository(YoutubeAccount::class)
52+
->findOneBy(['accountName' => $accountName]);
53+
54+
if (!$account) {
55+
$io->error("YouTube account not found: {$accountName}");
56+
$io->note('Available accounts:');
57+
58+
$accounts = $this->documentManager->getRepository(YoutubeAccount::class)->findAll();
59+
foreach ($accounts as $acc) {
60+
$io->writeln(" - {$acc->getAccountName()}");
61+
}
62+
63+
return Command::FAILURE;
64+
}
65+
66+
try {
67+
$authUrl = $this->clientFactory->getAuthorizationUrl($account);
68+
69+
$io->success('Authorization URL generated successfully!');
70+
$io->section('Step 1: Authorize Application');
71+
$io->writeln('Visit this URL in your browser:');
72+
$io->newLine();
73+
$io->writeln(" <href={$authUrl}>{$authUrl}</>");
74+
$io->newLine();
75+
76+
$io->section('Step 2: Get Authorization Code');
77+
$io->writeln('1. Click "Allow" to authorize the application');
78+
$io->writeln('2. You will be redirected to a URL like: http://localhost/?code=XXXXX');
79+
$io->writeln('3. Copy the code from the URL (everything after "code=")');
80+
81+
$io->section('Step 3: Save Token');
82+
$io->writeln("Run this command with the code:");
83+
$io->newLine();
84+
$io->writeln(" php bin/console youtube:account:token {$accountName} <CODE>");
85+
$io->newLine();
86+
87+
return Command::SUCCESS;
88+
} catch (\Exception $e) {
89+
$io->error('Error generating authorization URL: ' . $e->getMessage());
90+
return Command::FAILURE;
91+
}
92+
}
93+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pumukit\YoutubeBundle\Application\Command\Account;
6+
7+
use Doctrine\ODM\MongoDB\DocumentManager;
8+
use Pumukit\YoutubeBundle\Domain\Model\YoutubeAccount;
9+
use Symfony\Component\Console\Attribute\AsCommand;
10+
use Symfony\Component\Console\Command\Command;
11+
use Symfony\Component\Console\Input\InputArgument;
12+
use Symfony\Component\Console\Input\InputInterface;
13+
use Symfony\Component\Console\Input\InputOption;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
use Symfony\Component\Console\Style\SymfonyStyle;
16+
17+
#[AsCommand(
18+
name: 'youtube:account:create',
19+
description: 'Create a new YouTube account configuration'
20+
)]
21+
class CreateAccountCommand extends Command
22+
{
23+
public function __construct(
24+
private readonly DocumentManager $documentManager,
25+
private readonly string $projectDir
26+
) {
27+
parent::__construct();
28+
}
29+
30+
protected function configure(): void
31+
{
32+
$this
33+
->addArgument('name', InputArgument::REQUIRED, 'Account name (unique identifier)')
34+
->addArgument('credentials-file', InputArgument::REQUIRED, 'Path to OAuth credentials JSON file (relative to project root)')
35+
->addOption('channel-id', 'c', InputOption::VALUE_REQUIRED, 'YouTube Channel ID (optional, will be fetched if not provided)')
36+
->addOption('paused', 'p', InputOption::VALUE_NONE, 'Create account in paused state')
37+
->setHelp(<<<'EOF'
38+
The <info>%command.name%</info> command creates a new YouTube account configuration:
39+
40+
<info>php %command.full_name% my-account config/youtube_accounts/prod/credentials.json</info>
41+
42+
With channel ID:
43+
<info>php %command.full_name% my-account config/youtube_accounts/prod/credentials.json --channel-id=UCxxxxxxxx</info>
44+
45+
Create paused:
46+
<info>php %command.full_name% my-account config/youtube_accounts/prod/credentials.json --paused</info>
47+
48+
The credentials file should be a valid Google OAuth 2.0 client secret JSON file.
49+
EOF
50+
);
51+
}
52+
53+
protected function execute(InputInterface $input, OutputInterface $output): int
54+
{
55+
$io = new SymfonyStyle($input, $output);
56+
57+
$name = $input->getArgument('name');
58+
$credentialsFile = $input->getArgument('credentials-file');
59+
$channelId = $input->getOption('channel-id');
60+
$paused = $input->getOption('paused');
61+
62+
// Validate account name doesn't exist
63+
$existingAccount = $this->documentManager
64+
->getRepository(YoutubeAccount::class)
65+
->findOneBy(['accountName' => $name]);
66+
67+
if ($existingAccount) {
68+
$io->error("Account with name '{$name}' already exists!");
69+
return Command::FAILURE;
70+
}
71+
72+
// Validate credentials file
73+
$fullPath = $this->projectDir.'/'.$credentialsFile;
74+
if (!file_exists($fullPath)) {
75+
$io->error("Credentials file not found: {$fullPath}");
76+
return Command::FAILURE;
77+
}
78+
79+
// Validate JSON format
80+
$credentials = json_decode(file_get_contents($fullPath), true);
81+
if (!$credentials) {
82+
$io->error("Invalid JSON in credentials file");
83+
return Command::FAILURE;
84+
}
85+
86+
// Validate it's a valid OAuth credentials file
87+
if (!isset($credentials['web']['client_id']) && !isset($credentials['installed']['client_id'])) {
88+
$io->error("Invalid OAuth credentials format. Must contain 'web' or 'installed' configuration.");
89+
return Command::FAILURE;
90+
}
91+
92+
// If no channel ID provided, try to fetch it from YouTube API
93+
if (!$channelId) {
94+
$io->note('No channel ID provided. You can set it later with youtube:account:update');
95+
$channelId = 'PENDING'; // Temporary placeholder
96+
}
97+
98+
try {
99+
// Create account
100+
$account = YoutubeAccount::create(
101+
$name,
102+
$channelId,
103+
$credentialsFile,
104+
$paused
105+
);
106+
107+
$this->documentManager->persist($account);
108+
$this->documentManager->flush();
109+
110+
$io->success([
111+
"YouTube account '{$name}' created successfully!",
112+
"ID: {$account->getId()}",
113+
"Channel ID: {$channelId}",
114+
"Credentials: {$credentialsFile}",
115+
"Status: ".($paused ? 'PAUSED' : 'ACTIVE'),
116+
]);
117+
118+
if ($channelId === 'PENDING') {
119+
$io->warning([
120+
'Channel ID is set to PENDING.',
121+
'Please authorize the account and update the channel ID:',
122+
" php bin/console youtube:account:authorize {$name}",
123+
]);
124+
}
125+
126+
return Command::SUCCESS;
127+
} catch (\Exception $e) {
128+
$io->error("Failed to create account: {$e->getMessage()}");
129+
return Command::FAILURE;
130+
}
131+
}
132+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Pumukit\YoutubeBundle\Application\Command\Account;
6+
7+
use Doctrine\ODM\MongoDB\DocumentManager;
8+
use Pumukit\YoutubeBundle\Domain\Model\Publication;
9+
use Pumukit\YoutubeBundle\Domain\Model\YoutubeAccount;
10+
use Symfony\Component\Console\Attribute\AsCommand;
11+
use Symfony\Component\Console\Command\Command;
12+
use Symfony\Component\Console\Input\InputArgument;
13+
use Symfony\Component\Console\Input\InputInterface;
14+
use Symfony\Component\Console\Input\InputOption;
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
use Symfony\Component\Console\Style\SymfonyStyle;
17+
18+
#[AsCommand(
19+
name: 'youtube:account:delete',
20+
description: 'Delete a YouTube account configuration'
21+
)]
22+
class DeleteAccountCommand extends Command
23+
{
24+
public function __construct(
25+
private readonly DocumentManager $documentManager
26+
) {
27+
parent::__construct();
28+
}
29+
30+
protected function configure(): void
31+
{
32+
$this
33+
->addArgument('name', InputArgument::REQUIRED, 'Account name to delete')
34+
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force deletion even if there are active publications')
35+
->setHelp(<<<'EOF'
36+
The <info>%command.name%</info> command deletes a YouTube account:
37+
38+
<info>php %command.full_name% my-account</info>
39+
40+
Force deletion (even with active publications):
41+
<info>php %command.full_name% my-account --force</info>
42+
43+
<comment>WARNING: Deleting an account will NOT delete videos from YouTube.
44+
It only removes the account configuration from the system.</comment>
45+
EOF
46+
);
47+
}
48+
49+
protected function execute(InputInterface $input, OutputInterface $output): int
50+
{
51+
$io = new SymfonyStyle($input, $output);
52+
53+
$name = $input->getArgument('name');
54+
$force = $input->getOption('force');
55+
56+
// Find account
57+
$account = $this->documentManager
58+
->getRepository(YoutubeAccount::class)
59+
->findOneBy(['accountName' => $name]);
60+
61+
if (!$account) {
62+
$io->error("Account with name '{$name}' not found!");
63+
return Command::FAILURE;
64+
}
65+
66+
// Check for active publications
67+
$publicationsCount = $this->documentManager
68+
->getRepository(Publication::class)
69+
->createQueryBuilder()
70+
->field('youtubeAccountId')->equals($account->getId())
71+
->count()
72+
->getQuery()
73+
->execute();
74+
75+
if ($publicationsCount > 0 && !$force) {
76+
$io->error([
77+
"Cannot delete account '{$name}'.",
78+
"It has {$publicationsCount} associated publication(s).",
79+
"Use --force to delete anyway (this will NOT delete videos from YouTube).",
80+
]);
81+
return Command::FAILURE;
82+
}
83+
84+
// Show warning and ask for confirmation
85+
$io->warning([
86+
"You are about to delete the YouTube account: {$name}",
87+
"ID: {$account->getId()}",
88+
"Channel ID: {$account->getChannelId()}",
89+
]);
90+
91+
if ($publicationsCount > 0) {
92+
$io->caution("This account has {$publicationsCount} associated publication(s).");
93+
}
94+
95+
$io->note('This will NOT delete videos from YouTube, only the account configuration.');
96+
97+
if (!$io->confirm('Are you sure you want to continue?', false)) {
98+
$io->info('Deletion cancelled.');
99+
return Command::SUCCESS;
100+
}
101+
102+
try {
103+
$this->documentManager->remove($account);
104+
$this->documentManager->flush();
105+
106+
$io->success("YouTube account '{$name}' deleted successfully!");
107+
108+
if ($publicationsCount > 0) {
109+
$io->warning("The {$publicationsCount} associated publication(s) still exist in the database but are now orphaned.");
110+
}
111+
112+
return Command::SUCCESS;
113+
} catch (\Exception $e) {
114+
$io->error("Failed to delete account: {$e->getMessage()}");
115+
return Command::FAILURE;
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)