diff --git a/.github/workflows/pre-commit-action.yaml b/.github/workflows/pre-commit-action.yaml index 57e2557..5310566 100644 --- a/.github/workflows/pre-commit-action.yaml +++ b/.github/workflows/pre-commit-action.yaml @@ -10,7 +10,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: 3.8 - name: Install dependencies run: | python -m pip install pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b75137..0c87bb3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: rev: v2.31.0 hooks: - id: pyupgrade - args: ["--py37-plus"] + args: ["--py38-plus"] - repo: https://github.com/pycqa/isort rev: 5.10.1 hooks: diff --git a/Makefile b/Makefile index fc4a540..e5d5326 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,7 @@ test: ## Run tests requirements-dev: .requirements-dev ## Install dev requirements .requirements-dev: pip3 install --user --upgrade poetry + poetry run pip install --quiet --upgrade pip setuptools wheel poetry install touch .requirements-dev .requirements @@ -65,6 +66,7 @@ requirements-dev: .requirements-dev ## Install dev requirements requirements: .requirements ## Install requirements .requirements: pip3 install --user --upgrade poetry + poetry run pip install --quiet --upgrade pip setuptools wheel poetry install --no-dev touch .requirements diff --git a/README.md b/README.md index 26037f2..9334723 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,16 @@ The purpose of this tool is to allow you to download non-transcoded media from P ## Run via Docker -``` -export PLEXDL_USER=whoever -export PLEXDL_PASS=hunter2 -alias plexdl='docker run --rm -e PLEXDL_USER -e PLEXDL_PASS quay.io/danielhoherd/plexdl plexdl' -plexdl $movie_title +```sh +export PLEXDL_USERNAME=whoever +export PLEXDL_PASSWORD=hunter2 +alias plexdl="docker run --rm -e PLEXDL_USERNAME -e PLEXDL_PASSWORD quay.io/danielhoherd/plexdl plexdl --show-ratings" +plexdl "some movie title" ``` ## Local Installation -``` +```sh git clone https://github.com/danielhoherd/plexdl.git pip3 install ./plexdl ``` @@ -25,29 +25,58 @@ pip3 install ./plexdl ``` $ plexdl --help -Usage: plexdl [OPTIONS] TITLE +Usage: plexdl [OPTIONS] COMMAND [ARGS]... + + plexdl CLI. + +Options: + --install-completion [bash|zsh|fish|powershell|pwsh] + Install completion for the specified shell. + --show-completion [bash|zsh|fish|powershell|pwsh] + Show completion for the specified shell, to + copy it or customize the installation. + --help Show this message and exit. + +Commands: + get-server-info Show info about servers available to your account. + search Search for media in servers that are available to your... + +$ plexdl get-server-info --help +Usage: plexdl get-server-info [OPTIONS] [USERNAME] [PASSWORD] [DEBUG] + + Show info about servers available to your account. + +Arguments: + [USERNAME] [env var: PLEXDL_USERNAME] + [PASSWORD] [env var: PLEXDL_PASSWORD] + [DEBUG] [env var: PLEXDL_DEBUG;default: False] + +Options: + --help Show this message and exit. + +$ plexdl search --help +Usage: plexdl search [OPTIONS] TITLE + + Search for media in servers that are available to your account. - Searches your plex account for media matching the given string, then - prints out download commands. +Arguments: + TITLE [env var: PLEXDL_TITLE;required] Options: - -v Increase verbosity (max -vvvv) - -u, --username TEXT Your Plex username (env PLEXDL_USER) - -p, --password TEXT Your Plex password (env PLEXDL_PASS) - -r, --relay / --no-relay Output relay servers along with direct - servers - --item-prefix TEXT String to prefix to each item (eg: curl -o) - --server-info / --no-server-info - Output summary about each server - --summary / --no-summary Output summary about each result - --ratings / --no-ratings Output rating information for each result - --metadata / --no-metadata Output media metadata about each file for - each result - --help Show this message and exit.$ export PLEXDL_USER='foo' + -u, --username TEXT [env var: PLEXDL_USERNAME; required] + -p, --password TEXT [env var: PLEXDL_PASSWORD; required] + --item-prefix TEXT String to prefix to each item (eg: curl -o) + --show-summary Show media summary for each result + --show-ratings Show ratings for each result + --show-metadata Show file and codec metadata for each file in each result + --include-relays Output relay servers along with direct servers + -v, --verbose + --debug + --help Show this message and exit. ``` ``` -$ plexdl living +$ plexdl search living =============================================================================== Server: "demo-server-1" ------------------------------------------------------------------------------- @@ -56,15 +85,17 @@ Movie: Night of the Living Dead ``` ``` -$ plexdl --ratings --metadata --summary living +$ plexdl search --show-ratings --show-metadata --show-summary living =============================================================================== Server: "demo-server-2" ------------------------------------------------------------------------------- Movie: Night of the Living Dead +Year: 1968 +Studio: Image Ten Summary: A ragtag group of Pennsylvanians barricade themselves in an old farmhouse to remain safe from a bloodthirsty, flesh-eating breed of monsters who are ravaging the East Coast of the United States. +Content rating: NR Audience rating: 7.7 Critic rating: 5.5 -Rated: NR (1280x672, h264, ac3, 4208kbps) "Night of the Living Dead.mkv" "https://...some_url..." ``` @@ -73,5 +104,6 @@ Rated: NR - cleanup - tests -- add option to search for specific types of content +- add option to search only for specific types of content +- add option to search only specific serveres - don't try to name files with characters that would be invalid diff --git a/plexdl/__init__.py b/plexdl/__init__.py index a6fa930..9ebbab3 100644 --- a/plexdl/__init__.py +++ b/plexdl/__init__.py @@ -1,5 +1,5 @@ """Shim for package execution (python3 -m plexdl ...).""" -from .cli import main +from .cli import app if __name__ == "__main__": # pragma: no cover - main() + app() diff --git a/plexdl/cli.py b/plexdl/cli.py index beadb85..be65433 100644 --- a/plexdl/cli.py +++ b/plexdl/cli.py @@ -1,14 +1,19 @@ """plexdl CLI.""" +import datetime import logging import os import sys +import humanize import plexapi -from click import argument, command, echo, option, version_option +import typer from importlib_metadata import version __version__ = version(__package__) +from plexapi.myplex import MyPlexAccount +from plexapi.server import PlexServer + from plexdl.plexdl import Client @@ -17,40 +22,96 @@ def get_logger(ctx, param, value): logging.basicConfig(format="%(message)s") log = logging.getLogger("plexdl") if value > 0: - log.setLevel("DEBUG") # https://docs.python.org/3.7/library/logging.html#logging-levels + log.setLevel("DEBUG") # https://docs.python.org/3.9/library/logging.html#logging-levels return value -@command() -@option("-v", count=True, help="Be verbose", callback=get_logger) -@option("-u", "--username", help="Your Plex username (env PLEXDL_USER)", envvar="PLEXDL_USER") -@option("-p", "--password", help="Your Plex password (env PLEXDL_PASS)", envvar="PLEXDL_PASS") -@option("-r", "--relay/--no-relay", default=False, help="Output relay servers along with direct servers") -@option("--item-prefix", default="", help="String to prefix to each item (eg: curl -o)", envvar="PLEXDL_ITEM_PREFIX") -@option("--server-info/--no-server-info", default=False, help="Output summary about each server") -@option("--summary/--no-summary", default=False, help="Output summary about each result") -@option("--ratings/--no-ratings", default=False, help="Output rating information for each result") -@option("--metadata/--no-metadata", default=False, help="Output media metadata about each file for each result") -@version_option(version=__version__) -@argument("title", envvar="PLEXDL_TITLE") -def main(username, password, title, relay, server_info, item_prefix, summary, ratings, metadata, v): - """Search your plex account for media matching the given string, then prints out download commands.""" +app = typer.Typer(help=__doc__) + + +@app.command() +def get_server_info( + username: str = typer.Argument(None, envvar="PLEXDL_USERNAME"), + password: str = typer.Argument(None, envvar="PLEXDL_PASSWORD"), + debug: bool = typer.Argument(False, envvar="PLEXDL_DEBUG"), +): + """Show info about servers available to your account.""" + p = MyPlexAccount( + username=username, + password=password, + ) + servers = p.resources() + for server in servers: + if server.product == "Plex Media Server": + print(f"{server.name=} ", "-" * 70) + print(f" clientIdentifier: {server.clientIdentifier}") + last_seen_diff = datetime.datetime.now() - server.lastSeenAt + last_seen_time_delta = humanize.naturaldelta(last_seen_diff) + print(f" lastSeenAt: {server.lastSeenAt.strftime('%FT%T%z')} ({last_seen_time_delta})") + created_diff = datetime.datetime.now() - server.createdAt + created_time_delta = humanize.naturaldelta(created_diff) + print(f" createdAt: {server.createdAt.strftime('%FT%T%z')} ({created_time_delta})") + for i, connection in enumerate(server.connections): + preferred = connection.uri in server.preferred_connections() + print(f" connections[{i}]:") + print(f" local: {connection.local}") + print(f" uri: {connection.uri}") + print(f" httpuri: {connection.httpuri}") + print(f" relay: {bool(connection.relay)}") + print(f" preferred: {preferred}") + print(f" device: {server.device}") + print(f" home: {server.home}") + print(f" httpsRequired: {server.httpsRequired}") + print(f" name: {server.name}") + print(f" owned: {server.owned}") + print(f" ownerid: {server.ownerid}") + print(f" platform: {server.platform}") + print(f" platformVersion: {server.platformVersion}") + print(f" presence: {server.presence}") + print(f" product: {server.product}") + print(f" productVersion: {server.productVersion}") + print(f" provides: {server.provides}") + print(f" publicAddressMatches: {server.publicAddressMatches}") + print(f" sourceTitle: {server.sourceTitle}") + print(f" synced: {server.synced}") + print("") + + +@app.command() +def search( + username: str = typer.Option(..., "-u", "--username", envvar="PLEXDL_USERNAME"), + password: str = typer.Option(..., "-p", "--password", envvar="PLEXDL_PASSWORD"), + title: str = typer.Argument(..., envvar="PLEXDL_TITLE"), + item_prefix: str = typer.Option("", "--item-prefix", help="String to prefix to each item (eg: curl -o)"), + show_summary: bool = typer.Option(False, "--show-summary", help="Show media summary for each result"), + show_ratings: bool = typer.Option(False, "--show-ratings", help="Show ratings for each result"), + show_metadata: bool = typer.Option(False, "--show-metadata", help="Show file and codec metadata for each file in each result"), + include_relays: bool = typer.Option(False, "--include-relays", help="Output relay servers along with direct servers"), + verbose: bool = typer.Option(False, "--verbose", "-v"), + debug: bool = typer.Option(False, "--debug"), +): + """Search for media in servers that are available to your account.""" + p = Client( + username=username, + password=password, + title=title, + relay=include_relays, + debug=debug, + item_prefix=item_prefix, + summary=show_summary, + ratings=show_ratings, + metadata=show_metadata, + ) + p.main() + + +if __name__ == "__main__": try: - p = Client( - username=username, - password=password, - title=title, - relay=relay, - debug=v, - item_prefix=item_prefix, - server_info=server_info, - summary=summary, - ratings=ratings, - metadata=metadata, - ) - - p.main() + app() except KeyboardInterrupt: sys.exit(1) except plexapi.exceptions.BadRequest: sys.exit(2) + + +# https://typer.tiangolo.com/tutorial/subcommands/add-typer/ diff --git a/plexdl/plexdl.py b/plexdl/plexdl.py index e87202a..fccca63 100644 --- a/plexdl/plexdl.py +++ b/plexdl/plexdl.py @@ -23,7 +23,6 @@ def __init__(self, **kwargs): self.password = kwargs["password"] self.ratings = kwargs["ratings"] self.relay = kwargs["relay"] - self.server_info = kwargs["server_info"] self.summary = kwargs["summary"] self.title = kwargs["title"] self.username = kwargs["username"] @@ -58,7 +57,7 @@ def print_item_info(self, item, access_token): media_info.append(f"{length}") except ValueError: pass - if len(media_info) > 0: + if media_info: print(f'({", ".join(media_info)})') print(f' {self.item_prefix} "{download_filename}" "{download_url}"') @@ -69,14 +68,16 @@ def print_all_items_for_server(self, item, access_token): print("-" * 79) print(f"{item.TYPE.capitalize()}: {item.title}") if self.summary is True and len(item.summary) > 1: + print(f"Year: {item.year}") + print(f"Studio: {item.studio}") print(f"Summary: {item.summary}") if self.ratings is True: + if item.contentRating: + print(f"Content rating: {item.contentRating}") if item.audienceRating: print(f"Audience rating: {item.audienceRating}") if item.rating: print(f"Critic rating: {item.rating}") - if item.contentRating: - print(f"Rated: {item.contentRating}") self.print_item_info(self, item, access_token) elif item.TYPE in ["show"]: print("-" * 79) @@ -112,11 +113,6 @@ def main(self): print("\n") print("=" * 79) print(f'Server: "{this_server_connection.friendlyName}"{relay_status}') - if self.server_info is True: - print( - f'Plex version: {this_server_connection.version}\n"' - f"OS: {this_server_connection.platform} {this_server_connection.platformVersion}" - ) # TODO: add flags for each media type to help sort down what is displayed (since /hub/seach?mediatype="foo" doesn't work) # TODO: write handlers for each type diff --git a/pyproject.toml b/pyproject.toml index a441927..5717dd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "plexdl" -version = "0.3.2" +version = "0.4.0" description = "" authors = ["Daniel Hoherd "] license = "MIT License" @@ -9,26 +9,29 @@ classifiers=[ "Environment :: Console", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Topic :: Utilities", ] [tool.poetry.scripts] -plexdl = 'plexdl.cli:main' +plexdl = 'plexdl.cli:app' [tool.poetry.dependencies] -python = "^3.7" -click = "^8.0.3" -PlexAPI = "^4.8.0" humanfriendly = "^10.0" -requests = "^2.26.0" -importlib-metadata = "^4.8.2" +importlib-metadata = ">=4.2" +pendulum = "^2.0" +PlexAPI = "^4.8.0" +python = "^3.8" +requests = "^2.27.1" +typer = "^0.4.0" +humanize = "^3.13.1" [tool.poetry.dev-dependencies] -black = "^20.8b1" -flake8 = "^3.8.4" -tox = "^3.20.1" -pytest = "^6.1.1" +black = "^21.12b0" +flake8 = "^4.0.1" +pytest = "^6.2.5" +tox = "^3.24.5" +pdbpp = "^0.10.3" [tool.black] line-length = 132