Removed dependency on aioreactive. Doing this my way.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
29
poetry.lock
generated
29
poetry.lock
generated
@@ -1,14 +1,3 @@
|
||||
[[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"
|
||||
@@ -220,14 +209,6 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "expression"
|
||||
version = "1.1.0"
|
||||
description = "Practical functional programming for Python 3.8+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.0.12"
|
||||
@@ -1215,13 +1196,9 @@ notebook = ">=4.4.1"
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = ">=3.9,<3.11"
|
||||
content-hash = "2e75c062e40ed124d6ee8e3b3fa4ad143da1fe5ae1280ecc6cb09ae5b4dcd0f3"
|
||||
content-hash = "ddaa8d2b25ca8be048674b27b4e49951e5cf9271898f709761dff967f7cc48f3"
|
||||
|
||||
[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"},
|
||||
@@ -1425,10 +1402,6 @@ entrypoints = [
|
||||
{file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"},
|
||||
{file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
|
||||
]
|
||||
expression = [
|
||||
{file = "Expression-1.1.0-py3-none-any.whl", hash = "sha256:1179a4b2262d5d81565c0fcafbe722b09f8716a8aa88efe764846a563c6227b2"},
|
||||
{file = "Expression-1.1.0.tar.gz", hash = "sha256:2ddd230462351a48029c4e7d6a57c36df78eb3540a2c0b3e1a129dcd4fe75eb9"},
|
||||
]
|
||||
filelock = [
|
||||
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
|
||||
{file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
|
||||
|
||||
@@ -25,7 +25,6 @@ Changelog = "https://gitlab.com/xlorepdarkhelm/plex-playlist/releases"
|
||||
python = ">=3.9,<3.11"
|
||||
click = "^7.0"
|
||||
PlexAPI = "^4.5.2"
|
||||
aioreactive = "^0.15.0"
|
||||
appdirs = "^1.4.4"
|
||||
PyYAML = "^5.4.1"
|
||||
gino = "^1.0.1"
|
||||
|
||||
@@ -9,6 +9,8 @@ __all__ = ["APPNAME", "APPAUTHOR", "PATHS"]
|
||||
APPNAME = "plex-playlist"
|
||||
APPAUTHOR = "Cliff Hill"
|
||||
|
||||
DOWNLOAD_BATCH_SIZE = 1000
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Paths:
|
||||
|
||||
70
src/playlist/models.py
Normal file
70
src/playlist/models.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Contains the data models used throughout the application."""
|
||||
import dataclasses
|
||||
import datetime
|
||||
import functools
|
||||
import typing
|
||||
|
||||
import desert # type: ignore [import]
|
||||
import marshmallow # type: ignore [import]
|
||||
import plexapi.audio # type: ignore [import]
|
||||
|
||||
|
||||
class SchemaMeta(type):
|
||||
"""Metaclass to access the schema for the given class."""
|
||||
|
||||
schema: typing.ClassVar[marshmallow.schema.Schema]
|
||||
|
||||
@property # type: ignore [no-redef,misc]
|
||||
@functools.cache
|
||||
def schema(cls) -> marshmallow.schema.Schema:
|
||||
"""Return the schema object for this class."""
|
||||
return desert.schema(cls)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Track(metaclass=SchemaMeta):
|
||||
"""Model defining a Track object."""
|
||||
|
||||
id: int
|
||||
track_num: int
|
||||
title: str
|
||||
artist: str
|
||||
album_num: int
|
||||
album: str
|
||||
album_artist: str
|
||||
duration: int
|
||||
rating: typing.Optional[int]
|
||||
comments: str
|
||||
added: datetime.datetime
|
||||
play_count: int
|
||||
played: typing.Optional[datetime.datetime]
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls: typing.Type["Track"],
|
||||
data: dict[str, typing.Union[int, str, None]],
|
||||
) -> "Track":
|
||||
"""Load the given data into a Track instance."""
|
||||
return typing.cast(Track, cls.schema.load(data))
|
||||
|
||||
@classmethod
|
||||
def from_plex(
|
||||
cls: typing.Type["Track"],
|
||||
track: plexapi.audio.Track,
|
||||
) -> "Track":
|
||||
"""Convert a Plex track object to an internal Track object."""
|
||||
return cls(
|
||||
id=track.ratingKey,
|
||||
track_num=track.index,
|
||||
title=track.title,
|
||||
artist=track.artist().title,
|
||||
album_num=track.parentIndex,
|
||||
album=track.parentTitle,
|
||||
album_artist=track.grandparentTitle,
|
||||
duration=track.duration,
|
||||
rating=track.userRating,
|
||||
comments=track.summary,
|
||||
added=track.addedAt,
|
||||
play_count=track.viewCount,
|
||||
played=track.lastViewedAt,
|
||||
)
|
||||
@@ -1,79 +1,69 @@
|
||||
"""Contains the code to communicate to the Plex server."""
|
||||
import asyncio
|
||||
import functools
|
||||
import typing
|
||||
|
||||
import aioreactive as rx # type: ignore
|
||||
import plexapi.audio # type: ignore
|
||||
import plexapi.server # type: ignore
|
||||
import plexapi.audio # type: ignore [import]
|
||||
import plexapi.server # type: ignore [import]
|
||||
|
||||
from playlist import const
|
||||
from playlist import models
|
||||
from playlist import settings
|
||||
|
||||
__all__ = ["server", "gen_tracks"]
|
||||
__all__ = ["server", "gen_tracks", "total_track_count"]
|
||||
|
||||
server = plexapi.server.PlexServer(**settings.get().creds.to_dict())
|
||||
|
||||
|
||||
def gen_batched_tracks(
|
||||
def _downloader(
|
||||
pos: int,
|
||||
size: int,
|
||||
loop: asyncio.events.AbstractEventLoop,
|
||||
search_tracks: typing.Callable, # type: ignore [type-arg]
|
||||
) -> typing.Awaitable[list[plexapi.audio.Track]]:
|
||||
return loop.run_in_executor(
|
||||
None,
|
||||
functools.partial(
|
||||
search_tracks,
|
||||
maxresults=size,
|
||||
container_start=pos,
|
||||
container_size=size,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def gen_tracks(
|
||||
*,
|
||||
batch_size: int = 100,
|
||||
) -> typing.Generator[list[plexapi.audio.Track], None, None]:
|
||||
"""Generate Tracks from the server in `batch_size` batches."""
|
||||
pos = 0
|
||||
while 1:
|
||||
batch = list(
|
||||
server.library.section("Music").searchTracks(
|
||||
maxresults=batch_size,
|
||||
container_start=pos,
|
||||
container_size=batch_size,
|
||||
),
|
||||
)
|
||||
yield batch
|
||||
if len(batch) < batch_size:
|
||||
break
|
||||
pos += batch_size
|
||||
|
||||
|
||||
def track_mapper(
|
||||
tracks: list[plexapi.audio.Track],
|
||||
) -> list[dict[str, typing.Any]]:
|
||||
return [
|
||||
{
|
||||
"id": track.ratingKey,
|
||||
"track_num": track.index,
|
||||
"title": track.title,
|
||||
"artist": track.artist().title,
|
||||
"album_num": track.parentIndex,
|
||||
"album": track.parentTitle,
|
||||
"album_artist": track.grandparentTitle,
|
||||
"duration": track.duration,
|
||||
"rating": track.userRating,
|
||||
"comments": track.summary,
|
||||
"added": track.addedAt,
|
||||
"play_count": track.viewCount,
|
||||
"played": track.lastViewedAt,
|
||||
}
|
||||
for track in tracks
|
||||
]
|
||||
|
||||
|
||||
async def to_obs(
|
||||
tracks: list[dict[str, typing.Any]],
|
||||
) -> rx.AsyncRx[dict[str, typing.Any]]:
|
||||
return rx.AsyncRx.from_iterable(iter(tracks))
|
||||
|
||||
|
||||
def gen_tracks(
|
||||
*,
|
||||
batch_size: int = 100,
|
||||
) -> rx.AsyncIteratorObserver[dict[str, typing.Any]]:
|
||||
batch_size: int = const.DOWNLOAD_BATCH_SIZE,
|
||||
) -> typing.AsyncGenerator[models.Track, None]:
|
||||
"""Generate all Tracks from the Server, asynchronously.
|
||||
|
||||
Keyword Args:
|
||||
batch_size: determines how many Tracks are pulled from the Server at a time.
|
||||
"""
|
||||
source = rx.AsyncRx.from_iterable(gen_batched_tracks(batch_size=batch_size))
|
||||
track_rx = rx.pipe(
|
||||
source,
|
||||
rx.map(track_mapper),
|
||||
rx.flat_map_latest_async(to_obs),
|
||||
quotient, remainder = divmod(await total_track_count(), batch_size)
|
||||
loop = asyncio.get_running_loop()
|
||||
search_tracks = server.library.section("Music").searchTracks
|
||||
batches = [
|
||||
_downloader(ndx, batch_size, loop, search_tracks) for ndx in range(quotient)
|
||||
]
|
||||
batches.append(_downloader(quotient, remainder, loop, search_tracks))
|
||||
for batch in asyncio.as_completed(batches):
|
||||
batch_gen = (models.Track.from_plex(plex_track) for plex_track in await batch)
|
||||
for track in batch_gen:
|
||||
yield track
|
||||
|
||||
|
||||
async def total_track_count() -> int:
|
||||
"""Get the total number of tracks in the server."""
|
||||
loop = asyncio.get_running_loop()
|
||||
num_tracks: int = await loop.run_in_executor(
|
||||
None,
|
||||
(
|
||||
lambda: typing.cast(
|
||||
int,
|
||||
server.library.section("Music").totalViewSize("track"),
|
||||
)
|
||||
),
|
||||
)
|
||||
return rx.AsyncIteratorObserver(track_rx)
|
||||
return num_tracks
|
||||
|
||||
15
src/playlist/utils.py
Normal file
15
src/playlist/utils.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Utility functions & classes used in the application."""
|
||||
import contextlib
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def time_this() -> typing.Generator[None, None, None]:
|
||||
"""Context manager to time the execution of a block of code."""
|
||||
start = datetime.datetime.now()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
end = datetime.datetime.now()
|
||||
print(f"Elapsed time: {end - start}")
|
||||
Reference in New Issue
Block a user