Skip to content

Commit

Permalink
fix: itunes property track name (#12)
Browse files Browse the repository at this point in the history
* fix bug with itunes tag trackname and add additional tests

* chore: bump version

* docs: fix docstring error within pretty_list_of_basemodel_printer

* chore: add show_source for mkdocs site

* fix: remove deprecated warn() calls in YTDLP logger
  • Loading branch information
cboin1996 authored Apr 18, 2024
1 parent bb22009 commit 7e29676
Show file tree
Hide file tree
Showing 15 changed files with 261 additions and 35 deletions.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,7 @@ lint:
black tests

test:
python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=songbirdcore --cov-report=xml --cov-report=html tests/unit -v
python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=songbirdcore --cov-report=xml --cov-report=html tests/unit -v

test-stdout:
python -m pytest --cov=songbirdcore tests/unit -v
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ plugins:
show_root_full_path: true
show_root_toc_entry: true
docstring_section_style: list
show_source: false
show_source: true
show_bases: true
merge_init_into_class: true
show_symbol_type_heading: true
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "songbirdcore"
authors = [
{name = "Christian Boin"},
]
version = "0.0.7"
version = "0.0.8"
description = "core low level api for songbird"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
10 changes: 5 additions & 5 deletions songbirdcore/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
logger = logging.getLogger(__name__)


def load_version(toml_path: str):
def load_toml(toml_path: str):
with open(toml_path, "rb") as f:
data = tomllib.load(f)
return data
Expand Down Expand Up @@ -126,18 +126,18 @@ def find_file(path: str, filename: str) -> List[str]:


def pretty_list_of_basemodel_printer(
list_of_dicts: List[BaseModel], ignore_keys: Optional[List[str]] = None
list_of_models: List[BaseModel], ignore_keys: Optional[List[str]] = None
):
"""
renders a list to stdio given a list of pydantic BaseModel objects
Args:
list_of_dicts (List[BaseModel]): list of dictionaries to print
list_of_models (List[BaseModel]): list of pydantic models to print
ignore_keys (Optional[List[str]], optional): any keys/fields in BaseModel not to print
"""
i = len(list_of_dicts) - 1
i = len(list_of_models) - 1
logger.info("------------------------")
for element in reversed(list_of_dicts):
for element in reversed(list_of_models):
logger.info(i)
for k, v in element.model_dump().items():
if ignore_keys is not None:
Expand Down
18 changes: 2 additions & 16 deletions songbirdcore/itunes.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def mp3ID3Tagger(mp3_path: str, song_tag_data: itunes_api.ItunesApiSongModel) ->
audiofile = eyed3.load(mp3_path)
audiofile.tag.artist = song_tag_data.artistName
audiofile.tag.album = song_tag_data.collectionName
audiofile.tag.title = song_tag_data.TRACK_NAME
audiofile.tag.title = song_tag_data.trackName
audiofile.tag.genre = song_tag_data.primaryGenreName
audiofile.tag.track_num = (song_tag_data.trackNumber, song_tag_data.trackCount)
audiofile.tag.disc_num = (song_tag_data.discNumber, song_tag_data.discCount)
Expand Down Expand Up @@ -181,20 +181,6 @@ def mp3ID3Tagger(mp3_path: str, song_tag_data: itunes_api.ItunesApiSongModel) ->
return False


def convert_mp3_to_itunes_format(input_filename):
"""Convert the mp3 file to itunes format, updating tags to the new itunes standard.
Args:
input_filename (str): the full path of the file
Returns:
str: the path to the modified file.
"""
pydub.AudioSegment.ffmpeg = updates.get_path_to_ffmpeg()
song_file = pydub.AudioSegment.from_mp3(input_filename)
output_filename = input_filename.replace(".mp3", ".m4a")
song_file.export(output_filename, format="ipod")
return output_filename


def query_api(
search_variable: str, limit: int, mode: modes.Modes, lookup: bool = False
) -> List[Union[itunes_api.ItunesApiSongModel, itunes_api.ItunesApiAlbumKeys]]:
Expand Down Expand Up @@ -254,7 +240,7 @@ def query_api(

parsed_results_list.append(result)
except ValidationError as e:
logger.warn(
logger.warning(
f"Skipping the display of song at index [{index}] as it could not be loaded into expected format.\n{e}"
)

Expand Down
2 changes: 1 addition & 1 deletion songbirdcore/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version = "0.0.7"
version = "0.0.8"
4 changes: 2 additions & 2 deletions songbirdcore/youtube.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def get_video_links(
# Get the list of hrefs to each video on the home page
links = response.html.find("#video-title")
if len(links) == 0:
logger.warn(f"{tries+1}:{retry_count}.")
logger.warning(f"{tries+1}:{retry_count}.")
tries += 1
log_attempts = False
else:
Expand All @@ -68,7 +68,7 @@ def get_video_links(
logger.error(
f"Failed to get links from {youtube_home_url} after {retry_count} tries."
)
return None
return None, None

link_list = []
# create a user friendly list, containing videos with title and href refs.
Expand Down
Binary file added tests/unit/resources/empty.m4a
Binary file not shown.
Binary file added tests/unit/resources/empty.mp3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
114 changes: 114 additions & 0 deletions tests/unit/test_common.py
Original file line number Diff line number Diff line change
@@ -1 +1,115 @@
import pytest
import os, sys, shutil
from pydantic import BaseModel

from songbirdcore import common


@pytest.fixture
def create_test_folder():
tests_data_folder = os.path.join(sys.path[0], "tests-output")
if not os.path.exists(tests_data_folder):
os.mkdir(tests_data_folder)
yield tests_data_folder
# test cleanup
shutil.rmtree(tests_data_folder)


def test_load_data():
toml_path = os.path.join(sys.path[0], "..", "..", "pyproject.toml")
data = common.load_toml(toml_path)
assert data is not None


def test_set_logger_config_globally():
try:
common.set_logger_config_globally()
except Exception as e:
pytest.fail(f"Unexpected exception {e}!")


def test_name_plate():
try:
common.name_plate(entries=["foo"])
except Exception as e:
pytest.fail(f"Unexpected exception {e}!")


@pytest.fixture()
def create_dummy_file(create_test_folder):
fname = os.path.join(create_test_folder, "dummy.txt")

with open(fname, "w") as f:
f.write("hello")
yield fname


def fname_duper(create_dummy_file):

fname = common.fname_duper(fname=create_dummy_file, limit=2, count=1, dup_key="dup")

assert fname is not None


def test_fname_duper_fails(create_dummy_file):
fname = common.fname_duper(fname=create_dummy_file, limit=1, count=1, dup_key="dup")

assert fname is None


def test_remove_illegal_characters():
"""test remove illegal character"""
try:
result = common.remove_illegal_characters("\\\"/*?<>|':")
except Exception as e:
pytest.fail(f"Unexpected exception {e}!")

assert result == ""


def test_find_file(create_dummy_file):
"""test function for finding a file"""
result = common.find_file(
path=os.path.dirname(create_dummy_file),
filename=os.path.basename(create_dummy_file),
)

assert len(result) > 0


def test_pretty_list_of_basemodel_printer(capsys):
"""test function runs without exception"""

class Model(BaseModel):
foo: str = "bar"

try:
common.pretty_list_of_basemodel_printer(list_of_models=[Model()])
except Exception as e:
pytest.fail(f"Unexpected exception {e}!")


def test_pretty_list_of_baseModel_printer(capsys):
"""test ignore_keys functionality"""

class Model(BaseModel):
foo: str = "bar"
ignore_me: str = "ignore_me"

try:
common.pretty_list_of_basemodel_printer(
list_of_models=[Model()], ignore_keys=["ignore_me"]
)
except Exception as e:
pytest.fail(f"Unexpected exception {e}!")

captured = capsys.readouterr()
assert "ignore_me" not in captured.out


def tests_pretty_lst_printer():
"""test function runs without exception"""
try:
common.pretty_lst_printer(["hi", "there"])
except Exception as e:
pytest.fail(f"Unexpected exception: {e}")
119 changes: 119 additions & 0 deletions tests/unit/test_itunes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import pytest

from typing import List, Union
import shutil
import os, sys

from songbirdcore import itunes
from songbirdcore.models import modes, itunes_api
from test_common import create_test_folder

RESOURCES_FOLDER = "resources"


@pytest.fixture()
def query_api() -> (
List[Union[itunes_api.ItunesApiSongModel, itunes_api.ItunesApiAlbumKeys]]
):
"""pytest fixture that queries the itunes search
api for songs.
Yields:
List[Union[itunes_api.ItunesApiSongModel, itunes_api.ItunesApiAlbumKeys]]: a list of songs that match the album
"""
results = itunes.query_api(
search_variable="jolene", limit=20, mode=modes.Modes.SONG, lookup=False
)
yield results


@pytest.fixture()
def query_api_album() -> (
List[Union[itunes_api.ItunesApiSongModel, itunes_api.ItunesApiAlbumKeys]]
):
"""pytest fixture that queries the itunes
search api for an album, and returns
the songs in that album
Yields:
List[Union[itunes_api.ItunesApiSongModel, itunes_api.ItunesApiAlbumKeys]]: the list of song properties
"""
album_results = itunes.query_api(
search_variable="jolene", limit=20, mode=modes.Modes.ALBUM, lookup=False
)
# select first result by default
result = album_results[0]
song_results = itunes.query_api(
search_variable=result.collectionId,
limit=result.trackCount,
mode=modes.Modes.SONG,
lookup=True,
)

yield song_results


def test_query_api(query_api):
assert len(query_api) > 0


def test_query_api_album(query_api_album):
assert len(query_api_album) > 0


def test_m4a_tagger(create_test_folder, query_api):
"""test m4a tagging function executes
successfully
"""
input_fpath = os.path.join(sys.path[0], RESOURCES_FOLDER, "empty.m4a")
output_fpath = os.path.join(create_test_folder, "empty.m4a")
shutil.copy(input_fpath, output_fpath)
# query api, get the first result
tags = query_api[0]
result = itunes.m4a_tagger(file_path=output_fpath, song_tag_data=tags)

assert result == True


def test_mp3_tagger(create_test_folder, query_api):
"""test mp3 tagging function executes successfully"""
input_fpath = os.path.join(sys.path[0], RESOURCES_FOLDER, "empty.mp3")
output_fpath = os.path.join(create_test_folder, "empty.mp3")
shutil.copy(input_fpath, output_fpath)
# query api, get the first result
tags = query_api[0]
result = itunes.mp3ID3Tagger(mp3_path=output_fpath, song_tag_data=tags)

assert result == True


@pytest.fixture()
def get_itunes_lib_path():
return os.path.join(sys.path[0], RESOURCES_FOLDER, "mock-itunes-lib")


def test_itunes_lib_search_default(get_itunes_lib_path):
"""test itunes lib search functionality using mock itunes library"""
results = itunes.itunes_lib_search(
itunes_lib_path=get_itunes_lib_path,
search_parameters="empty",
album_properties=None,
)
assert len(results) > 0


def test_itunes_lib_search_with_props(query_api_album, get_itunes_lib_path):
"""test itunes lib search enhanced with album properties"""
props = query_api_album[0]
results = itunes.itunes_lib_search(
itunes_lib_path=get_itunes_lib_path,
search_parameters="jolene",
album_properties=props,
)
assert len(results) > 0


def test_artwork_searcher(query_api):
"""test getting artwork url given song properties"""
response = itunes.artwork_searcher(url=query_api[0].artworkUrl100)
assert response.status_code == 200
20 changes: 12 additions & 8 deletions tests/unit/test_web.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import pytest
from songbirdcore import web


class TestSimpleSession:
def setup_test(self):
# self.session = web.SimpleSession("youtube", root_url=youtube_home_url, headers=headers)
pass
@pytest.fixture()
def get_youtube_session():
return web.SimpleSession("youtube", root_url="https://www.youtube.com")

def test_get_form_inputs(self):
pass

def test_enter_search_form(self):
pass
def test_enter_search_form(get_youtube_session: str):
"""test entering youtube's main search from"""
session = get_youtube_session
response = session.enter_search_form(
search_url="https://www.youtube.com/results",
payload={"search_query": "billy joel"},
)
assert response is not None and response.status_code == 200

0 comments on commit 7e29676

Please sign in to comment.