Skip to content

Commit 2cab711

Browse files
committed
Merge branch 'feature/video-comments' into feature/video-livechat
2 parents 2b9a0ca + 1615924 commit 2cab711

File tree

10 files changed

+343
-52
lines changed

10 files changed

+343
-52
lines changed

tests/commands/conftest.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def channels_urls():
6+
return [
7+
"https://www.youtube.com/@Turicas/featured",
8+
"https://www.youtube.com/c/PythonicCaf%C3%A9"
9+
]
10+
11+
12+
@pytest.fixture
13+
def videos_ids():
14+
return [
15+
"video_id_1",
16+
"video_id_2"
17+
]
18+
19+
20+
@pytest.fixture
21+
def videos_urls(videos_ids):
22+
return [
23+
f"https://www.youtube.com/?v={video_id}" for video_id in videos_ids
24+
]
25+
26+
27+
@pytest.fixture
28+
def usernames():
29+
return ["Turicas", "PythonicCafe"]

tests/commands/test_base.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
import argparse
33
import pytest
44

5-
from io import StringIO
6-
from datetime import datetime
75
from pathlib import Path
86
from unittest.mock import MagicMock, patch, mock_open
97
from youtool.commands import Command
@@ -83,7 +81,7 @@ def test_data_from_csv_column_not_found(mock_csv_file):
8381
file_path = Path("tests/resources/csv_column_not_found.csv")
8482
with pytest.raises(Exception) as exc_info:
8583
Command.data_from_csv(file_path, "NonExistentColumn")
86-
assert f"Column NonExistentColumn not found on {file_path}" in str(exc_info.value)
84+
assert "Column NonExistentColumn not found on tests/resources/csv_column_not_found.csv" in str(exc_info.value)
8785

8886

8987
@pytest.fixture
@@ -126,3 +124,23 @@ def test_data_to_csv_output(tmp_path):
126124
assert Path(output_file_path).is_file()
127125
assert expected_output == Path(output_file_path).read_text()
128126
assert str(output_file_path) == result
127+
128+
def test_filter_fields():
129+
channel_info = {
130+
'channel_id': '123456',
131+
'channel_name': 'Test Channel',
132+
'subscribers': 1000,
133+
'videos': 50,
134+
'category': 'Tech'
135+
}
136+
137+
info_columns = ['channel_id', 'channel_name', 'subscribers']
138+
filtered_info = Command.filter_fields(channel_info, info_columns)
139+
140+
expected_result = {
141+
'channel_id': '123456',
142+
'channel_name': 'Test Channel',
143+
'subscribers': 1000
144+
}
145+
146+
assert filtered_info == expected_result, f"Expected {expected_result}, but got {filtered_info}"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pytest
2+
3+
from unittest.mock import patch, Mock, call
4+
5+
from youtool.commands.channel_info import ChannelInfo, YouTube
6+
7+
8+
def test_filter_fields():
9+
channel_info = {
10+
'channel_id': '123456',
11+
'channel_name': 'Test Channel',
12+
'subscribers': 1000,
13+
'videos': 50,
14+
'category': 'Tech'
15+
}
16+
17+
info_columns = ['channel_id', 'channel_name', 'subscribers']
18+
filtered_info = ChannelInfo.filter_fields(channel_info, info_columns)
19+
20+
expected_result = {
21+
'channel_id': '123456',
22+
'channel_name': 'Test Channel',
23+
'subscribers': 1000
24+
}
25+
26+
assert filtered_info == expected_result, f"Expected {expected_result}, but got {filtered_info}"
27+
28+
29+
def test_channel_ids_from_urls_and_usernames(mocker, channels_urls, usernames):
30+
ids_from_urls_mock = "id_from_url"
31+
ids_from_usernames_mock = "id_from_username"
32+
youtube_mock = mocker.patch("youtool.commands.channel_info.YouTube")
33+
34+
channel_id_from_url_mock = Mock(return_value=ids_from_urls_mock)
35+
channel_id_from_username_mock = Mock(return_value=ids_from_usernames_mock)
36+
channels_infos_mock = Mock(return_value=[])
37+
38+
youtube_mock.return_value.channel_id_from_url = channel_id_from_url_mock
39+
youtube_mock.return_value.channel_id_from_username = channel_id_from_username_mock
40+
youtube_mock.return_value.channels_infos = channels_infos_mock
41+
42+
ChannelInfo.execute(urls=channels_urls, usernames=usernames)
43+
44+
channel_id_from_url_mock.assert_has_calls(
45+
[call(url) for url in channels_urls]
46+
)
47+
channel_id_from_username_mock.assert_has_calls(
48+
[call(username) for username in usernames]
49+
)
50+
channels_infos_mock.assert_called_once_with([ids_from_urls_mock, ids_from_usernames_mock])
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import csv
2+
import pytest
3+
4+
from io import StringIO
5+
from datetime import datetime
6+
from unittest.mock import Mock
7+
from youtool.commands import VideoComments
8+
9+
10+
def test_video_comments(mocker):
11+
youtube_mock = mocker.patch("youtool.commands.video_comments.YouTube")
12+
video_id = "video_id_mock"
13+
14+
expected_result = [
15+
{"text": "my_comment", "author": "my_name"}
16+
]
17+
18+
csv_file = StringIO()
19+
csv_writer = csv.DictWriter(csv_file, fieldnames=expected_result[0].keys())
20+
csv_writer.writeheader()
21+
csv_writer.writerows(expected_result)
22+
23+
videos_comments_mock = Mock(return_value=expected_result)
24+
youtube_mock.return_value.video_comments = videos_comments_mock
25+
result = VideoComments.execute(id=video_id)
26+
27+
videos_comments_mock.assert_called_once_with(video_id)
28+
29+
assert result == csv_file.getvalue()
30+
31+
32+
def test_video_comments_with_file_output(mocker, tmp_path):
33+
youtube_mock = mocker.patch("youtool.commands.video_comments.YouTube")
34+
video_id = "video_id_mock"
35+
36+
expected_result = [
37+
{"text": "my_comment", "author": "my_name"}
38+
]
39+
40+
csv_file = StringIO()
41+
csv_writer = csv.DictWriter(csv_file, fieldnames=expected_result[0].keys())
42+
csv_writer.writeheader()
43+
csv_writer.writerows(expected_result)
44+
45+
timestamp = datetime.now().strftime("%f")
46+
output_file_name = f"output_{timestamp}.csv"
47+
output_file_path = tmp_path / output_file_name
48+
49+
videos_comments_mock = Mock(return_value=expected_result)
50+
youtube_mock.return_value.video_comments = videos_comments_mock
51+
52+
result_file_path = VideoComments.execute(id=video_id, output_file_path=output_file_path)
53+
54+
with open(result_file_path, "r") as result_csv_file:
55+
result_csv = result_csv_file.read()
56+
57+
videos_comments_mock.assert_called_once_with(video_id)
58+
59+
assert result_csv.replace("\r", "") == csv_file.getvalue().replace("\r", "")

tests/commands/test_video_info.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import csv
2+
import pytest
3+
4+
from unittest.mock import Mock
5+
from pathlib import Path
6+
from youtool.commands import VideoInfo
7+
8+
9+
@pytest.fixture
10+
def youtube_mock(mocker, mock_video_info):
11+
mock = mocker.patch("youtool.commands.video_info.YouTube")
12+
mock_instance = mock.return_value
13+
mock_instance.videos_infos = Mock(return_value=mock_video_info)
14+
return mock_instance
15+
16+
@pytest.fixture
17+
def mock_video_info():
18+
return [
19+
{"id": "tmrhPou85HQ", "title": "Title 1", "description": "Description 1", "published_at": "2021-01-01", "view_count": 100, "like_count": 10, "comment_count": 5},
20+
{"id": "qoI_x9fylaw", "title": "Title 2", "description": "Description 2", "published_at": "2021-02-01", "view_count": 200, "like_count": 20, "comment_count": 10}
21+
]
22+
23+
def test_execute_with_ids_and_urls(youtube_mock, mocker, tmp_path, mock_video_info):
24+
ids = ["tmrhPou85HQ", "qoI_x9fylaw"]
25+
urls = ["https://www.youtube.com/watch?v=tmrhPou85HQ&ab_channel=Turicas", "https://www.youtube.com/watch?v=qoI_x9fylaw&ab_channel=PythonicCaf%C3%A9"]
26+
output_file_path = tmp_path / "output.csv"
27+
28+
VideoInfo.execute(ids=ids, urls=urls, output_file_path=str(output_file_path), api_key="test_api_key")
29+
30+
assert Path(output_file_path).is_file()
31+
with open(output_file_path, 'r') as f:
32+
reader = csv.DictReader(f)
33+
csv_data = list(reader)
34+
35+
assert csv_data[0]["id"] == "tmrhPou85HQ"
36+
assert csv_data[1]["id"] == "qoI_x9fylaw"
37+
38+
def test_execute_missing_arguments():
39+
with pytest.raises(Exception) as exc_info:
40+
VideoInfo.execute(api_key="test_api_key")
41+
42+
assert str(exc_info.value) == "Either 'ids' or 'urls' must be provided for the video-info command"
43+
44+
def test_execute_with_input_file_path(youtube_mock, mocker, tmp_path, mock_video_info):
45+
input_csv_content = """video_id,video_url
46+
tmrhPou85HQ,https://www.youtube.com/watch?v=tmrhPou85HQ&ab_channel=Turicas
47+
qoI_x9fylaw,https://www.youtube.com/watch?v=qoI_x9fylaw&ab_channel=PythonicCaf%C3%A9
48+
"""
49+
input_file_path = tmp_path / "input.csv"
50+
output_file_path = tmp_path / "output.csv"
51+
52+
with open(input_file_path, 'w') as f:
53+
f.write(input_csv_content)
54+
55+
VideoInfo.execute(input_file_path=str(input_file_path), output_file_path=str(output_file_path), api_key="test_api_key")
56+
57+
assert Path(output_file_path).is_file()
58+
with open(output_file_path, 'r') as f:
59+
reader = csv.DictReader(f)
60+
csv_data = list(reader)
61+
62+
assert csv_data[0]["id"] == "tmrhPou85HQ"
63+
assert csv_data[1]["id"] == "qoI_x9fylaw"
64+
65+
66+
def test_execute_with_info_columns(youtube_mock, mocker, tmp_path, mock_video_info):
67+
ids = ["tmrhPou85HQ", "qoI_x9fylaw"]
68+
output_file_path = tmp_path / "output.csv"
69+
70+
VideoInfo.execute(ids=ids, output_file_path=str(output_file_path), api_key="test_api_key", info_columns="id,title")
71+
72+
assert Path(output_file_path).is_file()
73+
with open(output_file_path, 'r') as f:
74+
reader = csv.DictReader(f)
75+
csv_data = list(reader)
76+
77+
assert csv_data[0]["id"] == "tmrhPou85HQ"
78+
assert csv_data[0]["title"] == "Title 1"
79+
assert csv_data[1]["id"] == "qoI_x9fylaw"
80+
assert csv_data[1]["title"] == "Title 2"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import csv
2+
import pytest
3+
4+
from io import StringIO
5+
from unittest.mock import Mock, call
6+
from datetime import datetime
7+
8+
from youtool.commands.video_search import VideoSearch
9+
10+
11+
def test_video_search_string_output(mocker, videos_ids, videos_urls):
12+
youtube_mock = mocker.patch("youtool.commands.video_search.YouTube")
13+
expected_videos_infos = [
14+
{
15+
column: f"v_{index}" for column in VideoSearch.INFO_COLUMNS
16+
} for index, _ in enumerate(videos_ids)
17+
]
18+
19+
csv_file = StringIO()
20+
csv_writer = csv.DictWriter(csv_file, fieldnames=VideoSearch.INFO_COLUMNS)
21+
csv_writer.writeheader()
22+
csv_writer.writerows(expected_videos_infos)
23+
24+
videos_infos_mock = Mock(return_value=expected_videos_infos)
25+
youtube_mock.return_value.videos_infos = videos_infos_mock
26+
27+
result = VideoSearch.execute(ids=videos_ids, urls=videos_urls)
28+
29+
videos_infos_mock.assert_called_once_with(list(set(videos_ids)))
30+
assert result == csv_file.getvalue()
31+
32+
33+
def test_video_search_file_output(mocker, videos_ids, videos_urls, tmp_path):
34+
youtube_mock = mocker.patch("youtool.commands.video_search.YouTube")
35+
expected_videos_infos = [
36+
{
37+
column: f"v_{index}" for column in VideoSearch.INFO_COLUMNS
38+
} for index, _ in enumerate(videos_ids)
39+
]
40+
41+
expected_csv_file = StringIO()
42+
csv_writer = csv.DictWriter(expected_csv_file, fieldnames=VideoSearch.INFO_COLUMNS)
43+
csv_writer.writeheader()
44+
csv_writer.writerows(expected_videos_infos)
45+
46+
timestamp = datetime.now().strftime("%f")
47+
output_file_name = f"output_{timestamp}.csv"
48+
output_file_path = tmp_path / output_file_name
49+
50+
videos_infos_mock = Mock(return_value=expected_videos_infos)
51+
youtube_mock.return_value.videos_infos = videos_infos_mock
52+
53+
result_file_path = VideoSearch.execute(
54+
ids=videos_ids, urls=videos_urls, output_file_path=output_file_path
55+
)
56+
57+
with open(result_file_path, "r") as result_csv_file:
58+
result_csv = result_csv_file.read()
59+
60+
videos_infos_mock.assert_called_once_with(list(set(videos_ids)))
61+
assert result_csv.replace("\r", "") == expected_csv_file.getvalue().replace("\r", "")
62+
63+
64+
def test_video_search_no_id_and_url_error():
65+
with pytest.raises(Exception, match="Either 'ids' or 'urls' must be provided"):
66+
VideoSearch.execute(ids=None, urls=None)

youtool/commands/base.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ def parse_arguments(cls, subparsers: argparse._SubParsersAction) -> None:
5050
parser.add_argument(argument_name, **argument_copy)
5151
parser.set_defaults(func=cls.execute)
5252

53+
@staticmethod
54+
def filter_fields(video_info: Dict, info_columns: Optional[List] = None) -> Dict:
55+
"""Filters the fields of a dictionary containing video information based on specified columns.
56+
57+
Args:
58+
video_info (Dict): A dictionary containing video information.
59+
info_columns (Optional[List], optional): A list specifying which fields to include in the filtered output.
60+
If None, returns the entire video_info dictionary. Defaults to None.
61+
62+
Returns:
63+
A dictionary containing only the fields specified in info_columns (if provided)
64+
or the entire video_info dictionary if info_columns is None.
65+
"""
66+
return {
67+
field: value for field, value in video_info.items() if field in info_columns
68+
} if info_columns else video_info
69+
70+
5371
@classmethod
5472
def execute(cls, **kwargs) -> str: # noqa: D417
5573
"""Executes the command.
@@ -87,7 +105,7 @@ def data_from_csv(file_path: Path, data_column_name: Optional[str] = None) -> Li
87105

88106
if fieldnames is None:
89107
raise ValueError("Fieldnames is None")
90-
108+
91109
if data_column_name not in fieldnames:
92110
raise Exception(f"Column {data_column_name} not found on {file_path}")
93111
for row in reader:

youtool/commands/channel_info.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ def execute(cls: Self, **kwargs) -> str:
108108
] + [
109109
youtube.channel_id_from_username(username) for username in (usernames or []) if username
110110
]
111-
channel_ids = [channel_id for channel_id in channels_ids if channel_id]
111+
channel_ids = list(
112+
set([channel_id for channel_id in channels_ids if channel_id])
113+
)
112114

113115
return cls.data_to_csv(
114116
data=[
@@ -117,4 +119,4 @@ def execute(cls: Self, **kwargs) -> str:
117119
) for channel_info in (youtube.channels_infos(channel_ids) or [])
118120
],
119121
output_file_path=output_file_path
120-
)
122+
)

0 commit comments

Comments
 (0)