From 4846c76c187a113a1e18417aefcc86616e2d2c9a Mon Sep 17 00:00:00 2001 From: aninhasalesp Date: Thu, 6 Nov 2025 23:30:09 -0300 Subject: [PATCH 1/6] Fix regex for channel ID extraction --- youtool/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtool/__init__.py b/youtool/__init__.py index 28bbe83..a8cca7f 100644 --- a/youtool/__init__.py +++ b/youtool/__init__.py @@ -11,7 +11,7 @@ import isodate # TODO: implement duration parser to remove dependency? import requests -REGEXP_CHANNEL_ID = re.compile('"externalId":"([^"]+)"') +REGEXP_CHANNEL_ID = re.compile('"channelId":"([^"]+)"') REGEXP_LOCATION_RADIUS = re.compile(r"^[0-9.]+(?:m|km|ft|mi)$") REGEXP_NAIVE_DATETIME = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}$") REGEXP_DATETIME_MILLIS = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+") From 2d7b47f6e29b9258f1ef13e17dfebd5dd94ebe12 Mon Sep 17 00:00:00 2001 From: aninhasalesp Date: Fri, 7 Nov 2025 18:51:31 -0300 Subject: [PATCH 2/6] Implement YouTube CLI Tool with command structure and channel ID extraction --- youtool/cli.py | 46 ++++++++++++ youtool/commands/__init__.py | 10 +++ youtool/commands/base.py | 125 +++++++++++++++++++++++++++++++++ youtool/commands/channel_id.py | 97 +++++++++++++++++++++++++ 4 files changed, 278 insertions(+) create mode 100644 youtool/cli.py create mode 100644 youtool/commands/__init__.py create mode 100644 youtool/commands/base.py create mode 100644 youtool/commands/channel_id.py diff --git a/youtool/cli.py b/youtool/cli.py new file mode 100644 index 0000000..1403bef --- /dev/null +++ b/youtool/cli.py @@ -0,0 +1,46 @@ +import argparse +import os + +from youtool.commands import COMMANDS + + +def main(): + """Main function for the YouTube CLI Tool. + + This function sets up the argument parser for the CLI tool, including options for the YouTube API key and + command-specific subparsers. It then parses the command-line arguments, retrieving the YouTube API key + from either the command-line argument '--api-key' or the environment variable 'YOUTUBE_API_KEY'. If the API + key is not provided through any means, it raises an argparse.ArgumentError. + + Finally, the function executes the appropriate command based on the parsed arguments. If an exception occurs + during the execution of the command, it is caught and raised as an argparse error for proper handling. + + Raises: + argparse.ArgumentError: If the YouTube API key is not provided. + argparse.ArgumentError: If there is an error during the execution of the command. + """ + parser = argparse.ArgumentParser(description="CLI Tool for managing YouTube videos add playlists") + parser.add_argument("--api-key", type=str, help="YouTube API Key", dest="api_key") + parser.add_argument("--debug", type=bool, help="Debug mode", dest="debug") + + subparsers = parser.add_subparsers(required=True, dest="command", title="Command", help="Command to be executed") + + for command in COMMANDS: + command.parse_arguments(subparsers) + + args = parser.parse_args() + args.api_key = args.api_key or os.environ.get("YOUTUBE_API_KEY") + + if not args.api_key: + parser.error("YouTube API Key is required") + + try: + print(args.func(**args.__dict__)) + except Exception as error: + if args.debug: + raise error + parser.error(error) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/youtool/commands/__init__.py b/youtool/commands/__init__.py new file mode 100644 index 0000000..7828ee3 --- /dev/null +++ b/youtool/commands/__init__.py @@ -0,0 +1,10 @@ +from .base import Command +from .channel_id import ChannelId + +COMMANDS = [ + ChannelId +] + +__all__ = [ + "Command", "COMMANDS", "ChannelId", +] \ No newline at end of file diff --git a/youtool/commands/base.py b/youtool/commands/base.py new file mode 100644 index 0000000..c481df2 --- /dev/null +++ b/youtool/commands/base.py @@ -0,0 +1,125 @@ +import csv +import argparse + +from typing import List, Dict, Any, Optional +from io import StringIO +from pathlib import Path +from datetime import datetime + + +class Command: + """A base class for commands to inherit from, following a specific structure. + + Attributes: + name (str): The name of the command. + arguments (List[Dict[str, Any]]): A list of dictionaries, each representing an argument for the command. + """ + name: str + arguments: List[Dict[str, Any]] + + @classmethod + def generate_parser(cls, subparsers: argparse._SubParsersAction): + """Creates a parser for the command and adds it to the subparsers. + + Args: + subparsers (argparse._SubParsersAction): The subparsers action to add the parser to. + + Returns: + argparse.ArgumentParser: The parser for the command. + """ + return subparsers.add_parser(cls.name, help=cls.__doc__) + + @classmethod + def parse_arguments(cls, subparsers: argparse._SubParsersAction) -> None: + """Parses the arguments for the command and sets the command's execute method as the default function to call. + + Args: + subparsers (argparse._SubParsersAction): The subparsers action to add the parser to. + """ + parser = cls.generate_parser(subparsers) + groups = {} + + for argument in cls.arguments: + argument_copy = {**argument} + argument_name = argument_copy.pop("name") + + group_name = argument_copy.pop("mutually_exclusive_group", None) + if group_name: + if group_name not in groups: + groups[group_name] = parser.add_argument_group(group_name) + groups[group_name].add_argument(argument_name, **argument_copy) + else: + parser.add_argument(argument_name, **argument_copy) + parser.set_defaults(func=cls.execute) + + @classmethod + def execute(cls, **kwargs) -> str: # noqa: D417 + """Executes the command. + + This method should be overridden by subclasses to define the command's behavior. + + Args: + arguments (argparse.Namespace): The parsed arguments for the command. + """ + raise NotImplementedError() + + @staticmethod + def data_from_csv(file_path: Path, data_column_name: Optional[str] = None) -> List[str]: + """Extracts a list of URLs from a specified CSV file. + + Args: + file_path: The path to the CSV file containing the URLs. + data_column_name: The name of the column in the CSV file that contains the URLs. + If not provided, it defaults to `ChannelId.URL_COLUMN_NAME`. + + Returns: + A list of URLs extracted from the specified CSV file. + + Raises: + Exception: If the file path is invalid or the file cannot be found. + """ + data = [] + + if not file_path.is_file(): + raise FileNotFoundError(f"Invalid file path: {file_path}") + + with file_path.open('r', newline='') as csv_file: + reader = csv.DictReader(csv_file) + fieldnames = reader.fieldnames + + if fieldnames is None: + raise ValueError("Fieldnames is None") + + if data_column_name not in fieldnames: + raise Exception(f"Column {data_column_name} not found on {file_path}") + for row in reader: + value = row.get(data_column_name) + if value is not None: + data.append(str(value)) + return data + + @classmethod + def data_to_csv(cls, data: List[Dict], output_file_path: Optional[str] = None) -> str: + """Converts a list of channel IDs into a CSV file. + + Parameters: + channels_ids (List[str]): List of channel IDs to be written to the CSV. + output_file_path (str, optional): Path to the file where the CSV will be saved. If not provided, the CSV will be returned as a string. + channel_id_column_name (str, optional): Name of the column in the CSV that will contain the channel IDs. + If not provided, the default value defined in ChannelId.CHANNEL_ID_COLUMN_NAME will be used. + + Returns: + str: The path of the created CSV file or, if no path is provided, the contents of the CSV as a string. + """ + if output_file_path: + output_path = Path(output_file_path) + if output_path.is_dir(): + command_name = cls.name.replace("-", "_") + timestamp = datetime.now().strftime("%M%S%f") + output_file_path = output_path / f"{command_name}_{timestamp}.csv" + + with (Path(output_file_path).open('w', newline='') if output_file_path else StringIO()) as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=list(data[0].keys()) if data else []) + writer.writeheader() + writer.writerows(data) + return str(output_file_path) if output_file_path else csv_file.getvalue() \ No newline at end of file diff --git a/youtool/commands/channel_id.py b/youtool/commands/channel_id.py new file mode 100644 index 0000000..0051dd2 --- /dev/null +++ b/youtool/commands/channel_id.py @@ -0,0 +1,97 @@ + +from pathlib import Path + +from youtool import YouTube + +from .base import Command + + +class ChannelId(Command): + """Get channel IDs from a list of URLs (or CSV filename with URLs inside), generate CSV output (just the IDs).""" + name = "channel-id" + arguments = [ + { + "name": "--urls", + "type": str, + "help": "Channels urls", + "nargs": "*", + "mutually_exclusive_group": "input_source" + }, + { + "name": "--urls-file-path", + "type": str, + "help": "Channels urls csv file path", + "mutually_exclusive_group": "input_source" + }, + {"name": "--output-file-path", "type": str, "help": "Output csv file path"}, + {"name": "--url-column-name", "type": str, "help": "URL column name on csv input files"}, + {"name": "--id-column-name", "type": str, "help": "Channel ID column name on csv output files"} + ] + + URL_COLUMN_NAME: str = "channel_url" + CHANNEL_ID_COLUMN_NAME: str = "channel_id" + + @classmethod + def execute(cls, **kwargs) -> str: + """Execute the channel-id command to fetch YouTube channel IDs from URLs and save them to a CSV file. + + This method retrieves YouTube channel IDs from a list of provided URLs or from a file containing URLs. + It then saves these channel IDs to a CSV file if an output file path is specified. + + Args: + urls (list[str], optional): A list of YouTube channel URLs. Either this or urls_file_path must be provided. + urls_file_path (str, optional): Path to a CSV file containing YouTube channel URLs. + Requires url_column_name to specify the column with URLs. + output_file_path (str, optional): Path to the output CSV file where channel IDs will be saved. + If not provided, the result will be returned as a string. + api_key (str): The API key to authenticate with the YouTube Data API. + url_column_name (str, optional): The name of the column in the urls_file_path CSV file that contains the URLs. + Default is "url". + id_column_name (str, optional): The name of the column for channel IDs in the output CSV file. + Default is "channel_id". + + Returns: + str: A message indicating the result of the command. If output_file_path is specified, the message will + include the path to the generated CSV file. Otherwise, it will return the result as a string. + + Raises: + Exception: If neither urls nor urls_file_path is provided. + """ + urls = kwargs.get("urls") + urls_file_path = kwargs.get("urls_file_path") + output_file_path = kwargs.get("output_file_path") + api_key = kwargs.get("api_key") + + url_column_name = kwargs.get("url_column_name") + id_column_name = kwargs.get("id_column_name") + + urls = cls.resolve_urls(urls, urls_file_path, url_column_name) + + youtube = YouTube([api_key], disable_ipv6=True) + + channels_ids = [ + youtube.channel_id_from_url(url) for url in urls if url + ] + + result = cls.data_to_csv( + data=[ + { + (id_column_name or cls.CHANNEL_ID_COLUMN_NAME): channel_id + } for channel_id in channels_ids + ], + output_file_path=output_file_path + ) + + return result + + @classmethod + def resolve_urls(cls, urls, urls_file_path, url_column_name): + if urls_file_path and not urls: + urls = cls.data_from_csv( + file_path=Path(urls_file_path), + data_column_name=url_column_name or cls.URL_COLUMN_NAME + ) + + if not urls: + raise Exception("Either 'username' or 'url' must be provided for the channel-id command") + return urls \ No newline at end of file From 89983c78447a840c4ebd79bbb85c3d254f298cd3 Mon Sep 17 00:00:00 2001 From: aninhasalesp Date: Fri, 7 Nov 2025 19:14:48 -0300 Subject: [PATCH 3/6] Make lint --- youtool/cli.py | 6 +++--- youtool/commands/__init__.py | 10 ++++----- youtool/commands/base.py | 18 ++++++++-------- youtool/commands/channel_id.py | 39 ++++++++++++++-------------------- 4 files changed, 33 insertions(+), 40 deletions(-) diff --git a/youtool/cli.py b/youtool/cli.py index 1403bef..898daf0 100644 --- a/youtool/cli.py +++ b/youtool/cli.py @@ -22,7 +22,7 @@ def main(): parser = argparse.ArgumentParser(description="CLI Tool for managing YouTube videos add playlists") parser.add_argument("--api-key", type=str, help="YouTube API Key", dest="api_key") parser.add_argument("--debug", type=bool, help="Debug mode", dest="debug") - + subparsers = parser.add_subparsers(required=True, dest="command", title="Command", help="Command to be executed") for command in COMMANDS: @@ -33,7 +33,7 @@ def main(): if not args.api_key: parser.error("YouTube API Key is required") - + try: print(args.func(**args.__dict__)) except Exception as error: @@ -43,4 +43,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/youtool/commands/__init__.py b/youtool/commands/__init__.py index 7828ee3..eac5630 100644 --- a/youtool/commands/__init__.py +++ b/youtool/commands/__init__.py @@ -1,10 +1,10 @@ from .base import Command from .channel_id import ChannelId -COMMANDS = [ - ChannelId -] +COMMANDS = [ChannelId] __all__ = [ - "Command", "COMMANDS", "ChannelId", -] \ No newline at end of file + "Command", + "COMMANDS", + "ChannelId", +] diff --git a/youtool/commands/base.py b/youtool/commands/base.py index c481df2..cf3e7a9 100644 --- a/youtool/commands/base.py +++ b/youtool/commands/base.py @@ -1,19 +1,19 @@ -import csv import argparse - -from typing import List, Dict, Any, Optional +import csv +from datetime import datetime from io import StringIO from pathlib import Path -from datetime import datetime +from typing import Any, Dict, List, Optional class Command: """A base class for commands to inherit from, following a specific structure. - + Attributes: name (str): The name of the command. arguments (List[Dict[str, Any]]): A list of dictionaries, each representing an argument for the command. """ + name: str arguments: List[Dict[str, Any]] @@ -83,13 +83,13 @@ def data_from_csv(file_path: Path, data_column_name: Optional[str] = None) -> Li if not file_path.is_file(): raise FileNotFoundError(f"Invalid file path: {file_path}") - with file_path.open('r', newline='') as csv_file: + with file_path.open("r", newline="") as csv_file: reader = csv.DictReader(csv_file) fieldnames = reader.fieldnames if fieldnames is None: raise ValueError("Fieldnames is None") - + if data_column_name not in fieldnames: raise Exception(f"Column {data_column_name} not found on {file_path}") for row in reader: @@ -118,8 +118,8 @@ def data_to_csv(cls, data: List[Dict], output_file_path: Optional[str] = None) - timestamp = datetime.now().strftime("%M%S%f") output_file_path = output_path / f"{command_name}_{timestamp}.csv" - with (Path(output_file_path).open('w', newline='') if output_file_path else StringIO()) as csv_file: + with Path(output_file_path).open("w", newline="") if output_file_path else StringIO() as csv_file: writer = csv.DictWriter(csv_file, fieldnames=list(data[0].keys()) if data else []) writer.writeheader() writer.writerows(data) - return str(output_file_path) if output_file_path else csv_file.getvalue() \ No newline at end of file + return str(output_file_path) if output_file_path else csv_file.getvalue() diff --git a/youtool/commands/channel_id.py b/youtool/commands/channel_id.py index 0051dd2..916fe6d 100644 --- a/youtool/commands/channel_id.py +++ b/youtool/commands/channel_id.py @@ -1,4 +1,3 @@ - from pathlib import Path from youtool import YouTube @@ -8,24 +7,25 @@ class ChannelId(Command): """Get channel IDs from a list of URLs (or CSV filename with URLs inside), generate CSV output (just the IDs).""" + name = "channel-id" arguments = [ { - "name": "--urls", - "type": str, - "help": "Channels urls", - "nargs": "*", - "mutually_exclusive_group": "input_source" + "name": "--urls", + "type": str, + "help": "Channels urls", + "nargs": "*", + "mutually_exclusive_group": "input_source", }, { - "name": "--urls-file-path", - "type": str, - "help": "Channels urls csv file path", - "mutually_exclusive_group": "input_source" + "name": "--urls-file-path", + "type": str, + "help": "Channels urls csv file path", + "mutually_exclusive_group": "input_source", }, {"name": "--output-file-path", "type": str, "help": "Output csv file path"}, {"name": "--url-column-name", "type": str, "help": "URL column name on csv input files"}, - {"name": "--id-column-name", "type": str, "help": "Channel ID column name on csv output files"} + {"name": "--id-column-name", "type": str, "help": "Channel ID column name on csv output files"}, ] URL_COLUMN_NAME: str = "channel_url" @@ -69,17 +69,11 @@ def execute(cls, **kwargs) -> str: youtube = YouTube([api_key], disable_ipv6=True) - channels_ids = [ - youtube.channel_id_from_url(url) for url in urls if url - ] + channels_ids = [youtube.channel_id_from_url(url) for url in urls if url] result = cls.data_to_csv( - data=[ - { - (id_column_name or cls.CHANNEL_ID_COLUMN_NAME): channel_id - } for channel_id in channels_ids - ], - output_file_path=output_file_path + data=[{(id_column_name or cls.CHANNEL_ID_COLUMN_NAME): channel_id} for channel_id in channels_ids], + output_file_path=output_file_path, ) return result @@ -88,10 +82,9 @@ def execute(cls, **kwargs) -> str: def resolve_urls(cls, urls, urls_file_path, url_column_name): if urls_file_path and not urls: urls = cls.data_from_csv( - file_path=Path(urls_file_path), - data_column_name=url_column_name or cls.URL_COLUMN_NAME + file_path=Path(urls_file_path), data_column_name=url_column_name or cls.URL_COLUMN_NAME ) if not urls: raise Exception("Either 'username' or 'url' must be provided for the channel-id command") - return urls \ No newline at end of file + return urls From 62896c4279bd67e1c914331f71b363c5b38b7b83 Mon Sep 17 00:00:00 2001 From: aninhasalesp Date: Fri, 7 Nov 2025 19:59:10 -0300 Subject: [PATCH 4/6] Add entry points for youtool CLI in setup configuration --- setup.cfg | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.cfg b/setup.cfg index 77478cb..36ff81d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,10 @@ packages = find: python_requires = >=3.7 install_requires = file: requirements/base.txt +[options.entry_points] +console_scripts = + youtool = youtool.cli:main + [options.extras_require] cli = file: requirements/cli.txt dev = file: requirements/dev.txt From 660edaf5472911727bc271d160bac92dd41cb5c5 Mon Sep 17 00:00:00 2001 From: aninhasalesp Date: Fri, 7 Nov 2025 20:51:34 -0300 Subject: [PATCH 5/6] Update execute method documentation for channel-id command to clarify input options and error handling --- youtool/commands/channel_id.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/youtool/commands/channel_id.py b/youtool/commands/channel_id.py index 916fe6d..4056a5e 100644 --- a/youtool/commands/channel_id.py +++ b/youtool/commands/channel_id.py @@ -35,13 +35,16 @@ class ChannelId(Command): def execute(cls, **kwargs) -> str: """Execute the channel-id command to fetch YouTube channel IDs from URLs and save them to a CSV file. - This method retrieves YouTube channel IDs from a list of provided URLs or from a file containing URLs. - It then saves these channel IDs to a CSV file if an output file path is specified. - - Args: - urls (list[str], optional): A list of YouTube channel URLs. Either this or urls_file_path must be provided. - urls_file_path (str, optional): Path to a CSV file containing YouTube channel URLs. - Requires url_column_name to specify the column with URLs. + This command retrieves YouTube channel IDs from one of two possible inputs: + - a list of YouTube channel URLs (`--urls`), or + - a CSV file containing those URLs (`--urls-file-path`). + + Args: + urls (list[str]): List of YouTube channel URLs. + Mutually exclusive with `urls_file_path`. + urls_file_path (str): Path to a CSV file containing YouTube channel URLs. + Mutually exclusive with `urls`. + Requires url_column_name to specify the column with URLs. output_file_path (str, optional): Path to the output CSV file where channel IDs will be saved. If not provided, the result will be returned as a string. api_key (str): The API key to authenticate with the YouTube Data API. @@ -55,7 +58,7 @@ def execute(cls, **kwargs) -> str: include the path to the generated CSV file. Otherwise, it will return the result as a string. Raises: - Exception: If neither urls nor urls_file_path is provided. + ValueError: If neither `urls` nor `urls_file_path` is provided, or if both are provided at the same time. """ urls = kwargs.get("urls") urls_file_path = kwargs.get("urls_file_path") From c1a0f333ad6c287da91ad00889b6fd5f5b9c37ad Mon Sep 17 00:00:00 2001 From: aninhasalesp Date: Fri, 7 Nov 2025 22:27:10 -0300 Subject: [PATCH 6/6] Add debug mode option to CLI and update argument types for channel ID command --- youtool/cli.py | 2 +- youtool/commands/channel_id.py | 27 +++++++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/youtool/cli.py b/youtool/cli.py index 898daf0..517c150 100644 --- a/youtool/cli.py +++ b/youtool/cli.py @@ -21,7 +21,7 @@ def main(): """ parser = argparse.ArgumentParser(description="CLI Tool for managing YouTube videos add playlists") parser.add_argument("--api-key", type=str, help="YouTube API Key", dest="api_key") - parser.add_argument("--debug", type=bool, help="Debug mode", dest="debug") + parser.add_argument("--debug", help="Debug mode", dest="debug", default=False, action="store_true") subparsers = parser.add_subparsers(required=True, dest="command", title="Command", help="Command to be executed") diff --git a/youtool/commands/channel_id.py b/youtool/commands/channel_id.py index 4056a5e..5bf45ad 100644 --- a/youtool/commands/channel_id.py +++ b/youtool/commands/channel_id.py @@ -19,11 +19,11 @@ class ChannelId(Command): }, { "name": "--urls-file-path", - "type": str, + "type": Path, "help": "Channels urls csv file path", "mutually_exclusive_group": "input_source", }, - {"name": "--output-file-path", "type": str, "help": "Output csv file path"}, + {"name": "--output-file-path", "type": Path, "help": "Output csv file path"}, {"name": "--url-column-name", "type": str, "help": "URL column name on csv input files"}, {"name": "--id-column-name", "type": str, "help": "Channel ID column name on csv output files"}, ] @@ -42,16 +42,16 @@ def execute(cls, **kwargs) -> str: Args: urls (list[str]): List of YouTube channel URLs. Mutually exclusive with `urls_file_path`. - urls_file_path (str): Path to a CSV file containing YouTube channel URLs. + urls_file_path (Path): Path to a CSV file containing YouTube channel URLs. Mutually exclusive with `urls`. Requires url_column_name to specify the column with URLs. - output_file_path (str, optional): Path to the output CSV file where channel IDs will be saved. - If not provided, the result will be returned as a string. - api_key (str): The API key to authenticate with the YouTube Data API. - url_column_name (str, optional): The name of the column in the urls_file_path CSV file that contains the URLs. - Default is "url". - id_column_name (str, optional): The name of the column for channel IDs in the output CSV file. - Default is "channel_id". + output_file_path (Path, optional): Path to the output CSV file where channel IDs will be saved. + If not provided, the result will be returned as a string. + api_key (str): The API key to authenticate with the YouTube Data API. + url_column_name (str, optional): The name of the column in the urls_file_path CSV file that contains the URLs. + Default is "url". + id_column_name (str, optional): The name of the column for channel IDs in the output CSV file. + Default is "channel_id". Returns: str: A message indicating the result of the command. If output_file_path is specified, the message will @@ -60,7 +60,7 @@ def execute(cls, **kwargs) -> str: Raises: ValueError: If neither `urls` nor `urls_file_path` is provided, or if both are provided at the same time. """ - urls = kwargs.get("urls") + urls = kwargs.get("urls") or [] urls_file_path = kwargs.get("urls_file_path") output_file_path = kwargs.get("output_file_path") api_key = kwargs.get("api_key") @@ -83,11 +83,10 @@ def execute(cls, **kwargs) -> str: @classmethod def resolve_urls(cls, urls, urls_file_path, url_column_name): - if urls_file_path and not urls: - urls = cls.data_from_csv( + if urls_file_path: + urls += cls.data_from_csv( file_path=Path(urls_file_path), data_column_name=url_column_name or cls.URL_COLUMN_NAME ) - if not urls: raise Exception("Either 'username' or 'url' must be provided for the channel-id command") return urls