Skip to content
This repository has been archived by the owner on Jul 19, 2024. It is now read-only.

Commit

Permalink
feat: add a search command
Browse files Browse the repository at this point in the history
  • Loading branch information
Flowrey committed Dec 10, 2023
1 parent 11ce35f commit 292733c
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 28 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.5.0] - 2023-12-10
### Added
- Added a `search` command
### Changed
- Changed the style of the progressbar for status

## [0.4.1] - 2023-09-17
### Added
- Added a `--destination` flag
Expand Down Expand Up @@ -51,7 +57,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Refactoring of all code

[Unreleased]: https://github.com/flowrey/youtube-bz/compare/v0.4.1...HEAD
[Unreleased]: https://github.com/flowrey/youtube-bz/compare/v0.5.0...HEAD
[0.5.0]: https://github.com/flowrey/youtube-bz/releases/tag/v0.5.0
[0.4.1]: https://github.com/flowrey/youtube-bz/releases/tag/v0.4.1
[0.4.0]: https://github.com/flowrey/youtube-bz/releases/tag/v0.4.0
[0.3.4]: https://github.com/flowrey/youtube-bz/releases/tag/v0.3.4
Expand Down
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,26 @@ $ pip install youtube-bz

## How to use
Display help
```
```console
$ youtube-bz --help
```

You can search for an album with its MBID (see https://musicbrainz.org/doc/MusicBrainz_Identifier)
You can search for a release with its [MBID](https://musicbrainz.org/doc/MusicBrainz_Identifier) on [MusicBrainz](https://musicbrainz.org/) or
with the command search for instance to search the album 'Hybrid Theory' of 'Linkin Park' you can run:

```console
$ youtube-bz search 'Hybrid Theory' --artist 'Linkin Park'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━┓
┃ ID ┃ Title ┃ Artist ┃ Score ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━┩
│ 2a4174ab-f0b1-430e-b7e2-25c062b45573 │ Hybrid Theory │ Linkin Park │ 100 │
│ cce15358-9fa0-38b5-b143-c51ff7024b24 │ Hybrid Theory │ Linkin Park │ 100 │
└──────────────────────────────────────┴───────────────┴─────────────┴───────┘
```
$ youtube-bz download MBID

And to download it just run:
```console
$ youtube-bz download 2a4174ab-f0b1-430e-b7e2-25c062b45573MBID
```

## Author
Expand Down
9 changes: 8 additions & 1 deletion src/youtube_bz/api/musicbrainz/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ class Media(TypedDict):


Release = TypedDict(
"Release", {"artist-credit": List[ArtistCredit], "media": List[Media], "title": str}
"Release",
{
"artist-credit": List[ArtistCredit],
"media": List[Media],
"title": str,
"score": int,
"id": str,
},
)


Expand Down
3 changes: 2 additions & 1 deletion src/youtube_bz/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .download import download
from .search import search

__all__ = ["download"]
__all__ = ["download", "search"]
37 changes: 37 additions & 0 deletions src/youtube_bz/commands/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import Optional

from rich.console import Console
from rich.table import Table

from youtube_bz.api import musicbrainz as MusicBrainzAPI


def search(
query: str,
verbose: bool,
artist: Optional[str] = None,
artistname: Optional[str] = None,
):
client = MusicBrainzAPI.Client()
results = client.search_release(query, artistname=artistname, artist=artist)

if len(results["releases"]) == 0:
print("No data to display")
return

table = Table(title="Seach Results")
table.add_column("ID")
table.add_column("Title")
table.add_column("Artist")
table.add_column("Score")

for r in results["releases"]:
table.add_row(
f'[bold blue][link=https://musicbrainz.org/release/{r["id"]}]{r["id"]}[/link][/bold blue]',
r["title"],
r["artist-credit"][0]["name"],
str(r["score"]),
)

console = Console()
console.print(table)
45 changes: 33 additions & 12 deletions src/youtube_bz/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,41 @@ def run_command(args: argparse.Namespace):
verbose = args.verbose if "verbose" in args else False
if args.command == "download":
commands.download(args.mbid, verbose, args.destination)
elif args.command == "search":
commands.search(args.query, verbose, args.artist, args.artistname)
else:
print(f"Unknown command {args.command}")


def _add_download(subparsers: argparse._SubParsersAction) -> None:
p = subparsers.add_parser(
"download",
help="Download a release",
description="Find and download Youtube Videos associated to a release on MusicBrainz.",
)
p.add_argument("mbid", help="music brainz identifer of a release")
p.add_argument("--verbose", action="store_true")

p.add_argument("-d", "--destination", help="Path to the output directory")


def _add_search(subparsers: argparse._SubParsersAction) -> None:
p = subparsers.add_parser(
"search",
help="Search a release",
description="Find a release on MusicBrainz.",
)
p.add_argument("query", help="Lucene search query")
p.add_argument("--verbose", action="store_true")
p.add_argument(
"--artist",
help='(part of) the combined credited artist name for the release, including join phrases (e.g. "Artist X feat.")',
)
p.add_argument(
"--artistname", help="(part of) the name of any of the release artists "
)


def get_command_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="youtube-bz",
Expand All @@ -30,18 +61,8 @@ def get_command_parser() -> argparse.ArgumentParser:
dest="command",
description="Get help for commands with youtube-bz COMMAND --help",
)

download_parser = subparsers.add_parser(
"download",
help="Download a release",
description="Find and download Youtube Videos associated to a release on MusicBrainz.",
)
download_parser.add_argument("mbid", help="music brainz identifer of a release")
download_parser.add_argument("--verbose", action="store_true")

download_parser.add_argument(
"-d", "--destination", help="Path to the output directory"
)
_add_download(subparsers)
_add_search(subparsers)

parser.add_argument("--version", action="store_true", help="Print version and exit")

Expand Down
12 changes: 12 additions & 0 deletions tests/test_command_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ def test_get_no_best_match(mock_search_results): # type: ignore
"artist-credit": [artist_credit],
"media": [media],
"title": "bar",
"score": 100,
"id": "0000",
}
get_best_match(release, track)

Expand Down Expand Up @@ -78,6 +80,8 @@ def test_get_best_match(mock_search_results): # type: ignore
"artist-credit": [artist_credit],
"media": [media],
"title": "bar",
"score": 100,
"id": "0000",
}
get_best_match(release, track)

Expand All @@ -92,6 +96,8 @@ def test_fail_get_best_match_with_urlerror(mock_search_results): # type: ignore
"artist-credit": [artist_credit],
"media": [media],
"title": "bar",
"score": 100,
"id": "0000",
}
with pytest.raises(URLError):
get_best_match(release, track)
Expand All @@ -109,6 +115,8 @@ def test_fail_get_best_match_with_httperror(mock_search_results):
"artist-credit": [artist_credit],
"media": [media],
"title": "bar",
"score": 100,
"id": "0000",
}
with pytest.raises(HTTPError):
get_best_match(release, track)
Expand Down Expand Up @@ -162,6 +170,8 @@ def test_download(mock_search_results, mock_lookup_release, *_): # type: ignore
"artist-credit": [artist_credit],
"media": [media],
"title": "bar",
"score": 100,
"id": "0000",
}
mock_lookup_release.return_value = release

Expand Down Expand Up @@ -207,6 +217,8 @@ def test_download_failed(mock_search_results, mock_lookup_release, *_): # type:
"artist-credit": [artist_credit],
"media": [media],
"title": "bar",
"score": 100,
"id": "0000",
}
mock_lookup_release.return_value = release
mock_search_results.side_effect = ValueError
Expand Down
29 changes: 29 additions & 0 deletions tests/test_command_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from unittest.mock import Mock, patch

from youtube_bz import commands


@patch("youtube_bz.api.musicbrainz.Client.search_release")
def test_search_command(search_mock: Mock):
search_mock.return_value = {
"releases": [
{
"title": "amo",
"media": [],
"artist-credit": [{"name": "bmth"}],
"id": "0000",
"score": 100,
}
]
}
commands.search("query", False)
search_mock.assert_called_once()


@patch("youtube_bz.api.musicbrainz.Client.search_release")
def test_search_command_no_results(search_mock: Mock, capsys):
search_mock.return_value = {"releases": []}
commands.search("query", False)
search_mock.assert_called_once()
capured = capsys.readouterr()
assert "No data to display\n" == capured.out
32 changes: 22 additions & 10 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
import importlib.metadata
from argparse import Namespace
from unittest.mock import patch
from unittest.mock import Mock, patch

import pytest

from youtube_bz.main import cli, run_command


def test_get_version(capsys): # type: ignore
def test_get_version(capsys):
with pytest.raises(SystemExit):
cli(["--version"])

captured = capsys.readouterr() # type: ignore
assert captured.out.strip() == importlib.metadata.version("youtube-bz") # type: ignore
captured = capsys.readouterr()
assert captured.out.strip() == importlib.metadata.version("youtube-bz")


def test_print_help(capsys): # type: ignore
cli([])
def test_print_help(capsys):
assert cli([]) == 1


def test_verbose(capsys): # type: ignore
@patch("youtube_bz.main.commands.download")
def test_run_download_cmd(mock_download: Mock):
cli(["download", "mbid", "--verbose"])
mock_download.assert_called_once()


def test_run_unk_cmd(capsys): # type: ignore
@patch("youtube_bz.main.commands.search")
def test_run_search_cmd(mock_search: Mock):
cli(["search", "query", "--verbose"])
mock_search.assert_called_once()


def test_run_failed_cmd(capsys):
assert cli(["download", "bad_mbid", "--verbose"]) == 1


def test_run_unk_cmd(capsys):
ns = Namespace(command="foo")
run_command(ns)
captured = capsys.readouterr() # type: ignore
assert captured.out.strip() == "Unknown command foo" # type: ignore
captured = capsys.readouterr()
assert captured.out.strip() == "Unknown command foo"


@patch("youtube_bz.main.get_command_parser")
Expand Down

0 comments on commit 292733c

Please sign in to comment.