diff --git a/poetry.lock b/poetry.lock index b4927d2..ff1de22 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,8 +1,19 @@ +[[package]] +name = "aioreactive" +version = "0.15.0" +description = "Async/await Reactive Tools for Python 3.9+" +category = "main" +optional = false +python-versions = ">=3.9" + +[package.dependencies] +expression = "*" + [[package]] name = "appdirs" version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" +category = "main" optional = false python-versions = "*" @@ -144,6 +155,14 @@ packaging = "*" six = ">=1.9.0" webencodings = "*" +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "cffi" version = "1.14.5" @@ -155,6 +174,14 @@ python-versions = "*" [package.dependencies] pycparser = "*" +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "click" version = "7.1.2" @@ -230,6 +257,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "expression" +version = "0.48.0" +description = "Practical functional programming for Python 3.8+" +category = "main" +optional = false +python-versions = ">=3.8" + [[package]] name = "flake8" version = "3.9.2" @@ -640,6 +675,14 @@ python-versions = ">=3.5" gitdb = ">=4.0.1,<5" typing-extensions = ">=3.7.4.0" +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "iniconfig" version = "1.1.1" @@ -877,6 +920,40 @@ category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +[[package]] +name = "marshmallow" +version = "3.12.1" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==4.0.0)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.4)"] +lint = ["mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "marshmallow-dataclass" +version = "8.4.1" +description = "Python library to convert dataclasses into marshmallow schemas." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +marshmallow = ">=3.0.0,<4.0" +typing-inspect = "*" + +[package.extras] +dev = ["marshmallow-enum", "typeguard", "pre-commit (>=1.18,<2.0)", "sphinx", "pytest (>=5.4)", "pytest-mypy-plugins (>=1.2.0)", "typing-extensions (>=3.7.2,<3.8.0)"] +docs = ["sphinx"] +enum = ["marshmallow-enum"] +lint = ["pre-commit (>=1.18,<2.0)"] +tests = ["pytest (>=5.4)", "pytest-mypy-plugins (>=1.2.0)", "typing-extensions (>=3.7.2,<3.8.0)"] +union = ["typeguard"] + [[package]] name = "matplotlib-inline" version = "0.1.2" @@ -935,7 +1012,7 @@ dmypy = ["psutil (>=4.0)"] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +category = "main" optional = false python-versions = "*" @@ -1121,6 +1198,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "plexapi" +version = "4.5.2" +description = "Python bindings for the Plex API." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +requests = "*" + [[package]] name = "pluggy" version = "0.13.1" @@ -1296,7 +1384,7 @@ python-versions = "*" name = "pyyaml" version = "5.4.1" description = "YAML parser and emitter for Python" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" @@ -1350,6 +1438,24 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + [[package]] name = "rstcheck" version = "3.3.1" @@ -1484,10 +1590,35 @@ python-versions = "*" name = "typing-extensions" version = "3.10.0.0" description = "Backported and Experimental Type Hints for Python 3.5+" -category = "dev" +category = "main" optional = false python-versions = "*" +[[package]] +name = "typing-inspect" +version = "0.6.0" +description = "Runtime inspection utilities for typing module." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "urllib3" +version = "1.26.4" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotlipy (>=0.6.0)"] + [[package]] name = "wcwidth" version = "0.2.5" @@ -1530,9 +1661,13 @@ pyyaml = "*" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "0acf2f1a4bc4fc0f6f4f8dfbe399c55674a66989dfb2e1da0f5fbb00cd3ac24a" +content-hash = "fc572c902f999040800ae27bd9646aae5012c7d026b010788599af141ea2288c" [metadata.files] +aioreactive = [ + {file = "aioreactive-0.15.0-py3-none-any.whl", hash = "sha256:3c7b903594e1eb15f0c16c1cfa96031e6c81371f999c50b081fe81dda463486a"}, + {file = "aioreactive-0.15.0.tar.gz", hash = "sha256:f3724425faed6d6f41f1c10330246e16444d47670668bcc6c268946731b50a52"}, +] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -1596,6 +1731,10 @@ bleach = [ {file = "bleach-3.3.0-py2.py3-none-any.whl", hash = "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125"}, {file = "bleach-3.3.0.tar.gz", hash = "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433"}, ] +certifi = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] cffi = [ {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, @@ -1635,6 +1774,10 @@ cffi = [ {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, ] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, @@ -1720,6 +1863,10 @@ enum-compat = [ {file = "enum-compat-0.0.3.tar.gz", hash = "sha256:3677daabed56a6f724451d585662253d8fb4e5569845aafa8bb0da36b1a8751e"}, {file = "enum_compat-0.0.3-py3-none-any.whl", hash = "sha256:88091b617c7fc3bbbceae50db5958023c48dc40b50520005aa3bf27f8f7ea157"}, ] +expression = [ + {file = "Expression-0.48.0-py3-none-any.whl", hash = "sha256:df8d066fef445e25107dc53fe337a5c8fa87e794118bb2c4b15db4185c82f3f9"}, + {file = "Expression-0.48.0.tar.gz", hash = "sha256:aed4deaa7eab9921b1078570ac4d9140952dfd7313a08ba0700f8d0fb1df202d"}, +] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, @@ -1857,6 +2004,10 @@ gitpython = [ {file = "GitPython-3.1.15-py3-none-any.whl", hash = "sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867"}, {file = "GitPython-3.1.15.tar.gz", hash = "sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e"}, ] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -1972,6 +2123,14 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] +marshmallow = [ + {file = "marshmallow-3.12.1-py2.py3-none-any.whl", hash = "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01"}, + {file = "marshmallow-3.12.1.tar.gz", hash = "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040"}, +] +marshmallow-dataclass = [ + {file = "marshmallow_dataclass-8.4.1-py3-none-any.whl", hash = "sha256:035f4aa9f516ca3c14c9ae3905fe8370b14cea6462ec1a9d4451209a6117976e"}, + {file = "marshmallow_dataclass-8.4.1.tar.gz", hash = "sha256:26b6ef76646c6cd71df3163c7106ddeaab27d9fac355cad41046627d5c15cda0"}, +] matplotlib-inline = [ {file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"}, {file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"}, @@ -2066,6 +2225,10 @@ pickleshare = [ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] +plexapi = [ + {file = "PlexAPI-4.5.2-py3-none-any.whl", hash = "sha256:4ad62586659c4c904a7e32d583cbf3b5894405695650a925832173e668d617e4"}, + {file = "PlexAPI-4.5.2.tar.gz", hash = "sha256:0e8fbb9399597d512ba6303d094e9d16fcbb2ee3a2eb0560307f6da1f22d3922"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, @@ -2265,6 +2428,10 @@ regex = [ {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, ] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] rstcheck = [ {file = "rstcheck-3.3.1.tar.gz", hash = "sha256:92c4f79256a54270e0402ba16a2f92d0b3c15c8f4410cb9c57127067c215741f"}, ] @@ -2388,6 +2555,15 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] +typing-inspect = [ + {file = "typing_inspect-0.6.0-py2-none-any.whl", hash = "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0"}, + {file = "typing_inspect-0.6.0-py3-none-any.whl", hash = "sha256:3b98390df4d999a28cf5b35d8b333425af5da2ece8a4ea9e98f71e7591347b4f"}, + {file = "typing_inspect-0.6.0.tar.gz", hash = "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7"}, +] +urllib3 = [ + {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, + {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, +] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, diff --git a/pyproject.toml b/pyproject.toml index a001d8a..ba7da66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,20 @@ [tool.poetry] -name = "plex-playlist" +name = "playlist" version = "0.1.0" description = "Playlist generator for Plex music" authors = ["Cliff Hill "] [tool.black] line-length = 88 -target-version = ["py39"] +target-version = ["py38"] [tool.poetry.dependencies] python = "^3.9" +PlexAPI = "^4.5.2" +aioreactive = "^0.15.0" +appdirs = "^1.4.4" +PyYAML = "^5.4.1" +marshmallow-dataclass = "^8.4.1" [tool.poetry.dev-dependencies] wheel = "^0.36.2" diff --git a/src/playlist/__init__.py b/src/playlist/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/playlist/const.py b/src/playlist/const.py new file mode 100644 index 0000000..916f0d3 --- /dev/null +++ b/src/playlist/const.py @@ -0,0 +1,29 @@ +"""Application-level constants.""" + +import dataclasses +import pathlib + +import appdirs # type: ignore + +__all__ = ["APPNAME", "APPAUTHOR", "PATHS"] + +APPNAME = "plex-playlist" +APPAUTHOR = "Cliff Hill" + + +@dataclasses.dataclass +class Paths: + CONFIG: pathlib.Path + DATA: pathlib.Path + CACHE: pathlib.Path + LOG: pathlib.Path + + +dirs = appdirs.AppDirs(APPNAME, APPAUTHOR) + +PATHS = Paths( + CONFIG=pathlib.Path(dirs.user_config_dir), + DATA=pathlib.Path(dirs.user_data_dir), + CACHE=pathlib.Path(dirs.user_cache_dir), + LOG=pathlib.Path(dirs.user_log_dir), +) diff --git a/src/playlist/main.py b/src/playlist/main.py new file mode 100644 index 0000000..29b8e10 --- /dev/null +++ b/src/playlist/main.py @@ -0,0 +1,9 @@ +import plexapi.server + +from playlist import settings + +creds = settings.get_settings().creds + +plex = plexapi.server.PlexServer(creds.url, creds.token) + +print(plex) diff --git a/src/playlist/settings.py b/src/playlist/settings.py new file mode 100644 index 0000000..6921558 --- /dev/null +++ b/src/playlist/settings.py @@ -0,0 +1,88 @@ +"""Define and control the Settings object for the playlist app.""" +import dataclasses +import typing + +import marshmallow +import marshmallow_dataclass +import yaml + +from playlist import const + +__all__ = ["get_settings"] + +SETTINGS_PATH = const.PATHS.CONFIG / "settings.yaml" + + +def _read_settings() -> dict: + with SETTINGS_PATH.open() as fp: + return yaml.safe_load(fp) + + +def _write_settings(data: dict) -> None: + with SETTINGS_PATH.open(mode="w") as fp: + fp.write(yaml.dump(data)) + + +def _refresh_settings(data: dict) -> dict: + _write_settings(data) + return _read_settings() + + +@marshmallow_dataclass.dataclass +class Creds: + """Credentials component for Settings object.""" + + url: str + token: str + + Schema: typing.ClassVar[ + typing.Type[marshmallow.Schema] # noqa: S101 + ] = marshmallow.Schema # For the type checker + + +def _make_creds() -> Creds: + import getpass + + import plexapi.myplex # type: ignore + + username = input("Plex Username: ") + password = getpass.getpass("Plex Password: ") + server = input("Plex Server: ") + + account = plexapi.myplex.MyPlexAccount(username, password) + plex = account.resource(server).connect() + data = {"url": plex._baseurl, "token": plex._token} + return Creds.Schema().load(data) + + +@marshmallow_dataclass.dataclass +class Settings: + """Settings object, loaded from settings.yaml file.""" + + creds: Creds = dataclasses.field(default_factory=_make_creds) + + Schema: typing.ClassVar[ + typing.Type[marshmallow.Schema] # noqa: S101 + ] = marshmallow.Schema # For the type checker + + _instance: typing.ClassVar["Settings"] = dataclasses.field( + metadata={"required": False}, + ) + + +def get_settings() -> Settings: + """Get the Settings object.""" + try: + return Settings._instance + except AttributeError: + try: + data = _read_settings() + except FileNotFoundError: + const.PATHS.CONFIG.mkdir(parents=True, exist_ok=True) + data = _refresh_settings({}) + + Settings._instance = Settings.Schema().load(data) + new_data = Settings.Schema().dump(Settings._instance) + if new_data != data: + _write_settings(new_data) + return Settings._instance