Skip to content

Commit

Permalink
Merge pull request #277 from Taxel/0.7.x
Browse files Browse the repository at this point in the history
  • Loading branch information
glensc authored Apr 19, 2021
2 parents c858c5a + f34d674 commit 0f37555
Show file tree
Hide file tree
Showing 17 changed files with 326 additions and 36 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ verify_ssl = true
click = "==7.1.2"
plexapi = "==4.5.0"
python-dotenv = "==0.15.0"
python-git-info = "==0.6.1"
requests-cache = "==0.5.2"
trakt = "==3.1.0"

Expand Down
9 changes: 8 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions plex_trakt_sync/commands/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from plex_trakt_sync.config import CONFIG
from plex_trakt_sync.plex_api import PlexApi
from plex_trakt_sync.trakt_api import TraktApi
from plex_trakt_sync.version import git_version_info


@click.command()
Expand All @@ -12,6 +13,9 @@ def inspect(input):
Inspect details of an object
"""

git_version = git_version_info() or 'Unknown version'
print(f"PlexTraktSync inspect [{git_version}]")

url = CONFIG["PLEX_BASEURL"]
token = CONFIG["PLEX_TOKEN"]
server = PlexServer(url, token)
Expand Down
33 changes: 25 additions & 8 deletions plex_trakt_sync/commands/plex_login.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from datetime import datetime, timedelta
from functools import partial
from typing import List

import click
from click import Choice
from plexapi.exceptions import Unauthorized, NotFound
from plexapi.myplex import MyPlexAccount, MyPlexResource
from plexapi.myplex import MyPlexAccount, MyPlexResource, ResourceConnection
from plexapi.server import PlexServer

from plex_trakt_sync.config import CONFIG
from plex_trakt_sync.style import prompt, error, success, title, comment
from plex_trakt_sync.style import prompt, error, success, title, comment, disabled, highlight

PROMPT_PLEX_PASSWORD = prompt("Please enter your Plex password")
PROMPT_PLEX_USERNAME = prompt("Please enter your Plex username")
Expand Down Expand Up @@ -55,23 +57,35 @@ def choose_managed_user(account: MyPlexAccount):


def prompt_server(servers: List[MyPlexResource]):
old_age = datetime.now() - timedelta(weeks=1)

def fmt_server(s):
details = comment(f"{s.product}/{s.productVersion} on {s.device}: {s.platform}/{s.platformVersion}")
return f"- {s.name}: [Last seen: {comment(str(s.lastSeenAt))}, Server: {details}]"
if s.lastSeenAt < old_age:
decorator = disabled
else:
decorator = comment

product = decorator(f"{s.product}/{s.productVersion}")
platform = decorator(f"{s.device}: {s.platform}/{s.platformVersion}")
click.echo(f"- {highlight(s.name)}: [Last seen: {decorator(str(s.lastSeenAt))}, Server: {product} on {platform}]")
c: ResourceConnection
for c in s.connections:
click.echo(f" {c.uri}")

owned_servers = [s for s in servers if s.owned]
unowned_servers = [s for s in servers if not s.owned]
sorter = partial(sorted, key=lambda s: s.lastSeenAt)

server_names = []
if owned_servers:
click.echo(success(f"{len(owned_servers)} owned servers found:"))
for s in owned_servers:
click.echo(fmt_server(s))
for s in sorter(owned_servers):
fmt_server(s)
server_names.append(s.name)
if unowned_servers:
click.echo(success(f"{len(owned_servers)} unowned servers found:"))
for s in unowned_servers:
click.echo(fmt_server(s))
for s in sorter(unowned_servers):
fmt_server(s)
server_names.append(s.name)

return click.prompt(
Expand Down Expand Up @@ -105,6 +119,9 @@ def choose_server(account: MyPlexAccount):
server = pick_server(account)
# Connect to obtain baseUrl
click.echo(title(f"Attempting to connect to {server.name}. This may take time and print some errors."))
click.echo(title(f"Server connections:"))
for c in server.connections:
click.echo(f" {c.uri}")
plex = server.connect()
# Validate connection again, the way we connect
plex = PlexServer(token=server.accessToken, baseurl=plex._baseurl)
Expand Down
31 changes: 23 additions & 8 deletions plex_trakt_sync/commands/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from plex_trakt_sync.trakt_api import TraktApi
from plex_trakt_sync.trakt_list_util import TraktListUtil
from plex_trakt_sync.logging import logger
from plex_trakt_sync.version import git_version_info


def sync_collection(pm, tm, trakt: TraktApi, trakt_movie_collection):
Expand All @@ -18,7 +19,7 @@ def sync_collection(pm, tm, trakt: TraktApi, trakt_movie_collection):
if tm.trakt in trakt_movie_collection:
return

logger.info(f"Add to Trakt Collection: {pm}")
logger.info(f"To be added to collection: {pm}")
trakt.add_to_collection(tm, pm)


Expand Down Expand Up @@ -136,6 +137,7 @@ def find_show_episodes(show, plex: PlexApi, trakt: TraktApi):


def for_each_show_episode(pm, tm, trakt: TraktApi):
lookup = trakt.lookup(tm)
for pe in pm.episodes():
try:
provider = pe.provider
Expand All @@ -147,19 +149,19 @@ def for_each_show_episode(pm, tm, trakt: TraktApi):
logger.error(f"Skipping {pe}: Provider {provider} not supported")
continue

te = trakt.find_episode(tm, pe)
te = trakt.find_episode(tm, pe, lookup)
if te is None:
logger.warning(f"Skipping {pe}: Not found on Trakt")
continue
yield tm, pe, te


def sync_all(movies=True, tv=True, show=None):
def sync_all(library=None, movies=True, tv=True, show=None, batch_size=None):
with requests_cache.disabled():
server = get_plex_server()
listutil = TraktListUtil()
plex = PlexApi(server)
trakt = TraktApi()
trakt = TraktApi(batch_size=batch_size)

with measure_time("Loaded Trakt lists"):
trakt_watched_movies = trakt.watched_movies
Expand All @@ -180,7 +182,7 @@ def sync_all(movies=True, tv=True, show=None):
logger.info("Recently added: {}".format(server.library.recentlyAdded()[:5]))

if movies:
for pm, tm in for_each_pair(plex.movie_sections, trakt):
for pm, tm in for_each_pair(plex.movie_sections(library=library), trakt):
sync_collection(pm, tm, trakt, trakt_movie_collection)
sync_ratings(pm, tm, plex, trakt)
sync_watched(pm, tm, plex, trakt, trakt_watched_movies)
Expand All @@ -189,7 +191,7 @@ def sync_all(movies=True, tv=True, show=None):
if show:
it = find_show_episodes(show, plex, trakt)
else:
it = for_each_episode(plex.show_sections, trakt)
it = for_each_episode(plex.show_sections(library=library), trakt)

for tm, pe, te in it:
sync_show_collection(tm, pe, te, trakt)
Expand All @@ -205,6 +207,10 @@ def sync_all(movies=True, tv=True, show=None):


@click.command()
@click.option(
"--library",
help="Specify Library to use"
)
@click.option(
"--show", "show",
type=str,
Expand All @@ -216,11 +222,20 @@ def sync_all(movies=True, tv=True, show=None):
default="all",
show_default=True, help="Specify what to sync"
)
def sync(sync_option: str, show: str):
@click.option(
"--batch-size", "batch_size",
type=int,
default=1, show_default=True,
help="Batch size for collection submit queue"
)
def sync(sync_option: str, library: str, show: str, batch_size: int):
"""
Perform sync between Plex and Trakt
"""

git_version = git_version_info()
if git_version:
logger.info(f"PlexTraktSync [{git_version}]")
logger.info(f"Syncing with Plex {CONFIG['PLEX_USERNAME']} and Trakt {CONFIG['TRAKT_USERNAME']}")

movies = sync_option in ["all", "movies"]
Expand All @@ -237,4 +252,4 @@ def sync(sync_option: str, show: str):
logger.info(f"Syncing TV={tv}, Movies={movies}")

with measure_time("Completed full sync"):
sync_all(movies=movies, tv=tv, show=show)
sync_all(movies=movies, library=library, tv=tv, show=show, batch_size=batch_size)
4 changes: 2 additions & 2 deletions plex_trakt_sync/decorators/rate_limit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from time import sleep, time

from requests.exceptions import ConnectionError
from trakt.errors import RateLimitException
from trakt.errors import RateLimitException, TraktInternalException
from plex_trakt_sync.logging import logger

last_time = None
Expand Down Expand Up @@ -41,7 +41,7 @@ def wrapper(*args, **kwargs):
try:
respect_trakt_rate()
return fn(*args, **kwargs)
except (RateLimitException, ConnectionError) as e:
except (RateLimitException, ConnectionError, TraktInternalException) as e:
if retry == retries:
raise e

Expand Down
12 changes: 6 additions & 6 deletions plex_trakt_sync/plex_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,24 +171,24 @@ class PlexApi:
def __init__(self, plex):
self.plex = plex

@property
@memoize
def movie_sections(self):
def movie_sections(self, library=None):
result = []
for section in self.library_sections:
if not type(section) is MovieSection:
continue
if library and section.title != library:
continue
result.append(PlexLibrarySection(section))

return result

@property
@memoize
def show_sections(self):
def show_sections(self, library=None):
result = []
for section in self.library_sections:
if not type(section) is ShowSection:
continue
if library and section.title != library:
continue
result.append(PlexLibrarySection(section))

return result
Expand Down
4 changes: 3 additions & 1 deletion plex_trakt_sync/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@
prompt = partial(click.style, fg="yellow")
success = partial(click.style, fg="green")
error = partial(click.style, fg="red")
comment = partial(click.style, fg="cyan")
comment = partial(click.style, fg="bright_cyan")
disabled = partial(click.style, fg="blue")
highlight = partial(click.style, fg="bright_white")
44 changes: 35 additions & 9 deletions plex_trakt_sync/trakt_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ class TraktApi:
Trakt API class abstracting common data access and dealing with requests cache.
"""

def __init__(self):
self.batch = TraktBatch(self)
def __init__(self, batch_size=None):
self.batch = TraktBatch(self, batch_size=batch_size)

@property
@memoize
Expand Down Expand Up @@ -216,14 +216,18 @@ def search_by_id(self, media_id: str, id_type: str, media_type: str):

return None

def find_episode(self, tm: TVShow, pe: PlexLibraryItem):
def find_episode(self, tm: TVShow, pe: PlexLibraryItem, lookup=None):
"""
Find Trakt Episode from Plex Episode
"""
lookup = self.lookup(tm)
lookup = lookup if lookup else self.lookup(tm)
try:
return lookup[pe.season_number][pe.episode_number].instance
except KeyError:
# Retry using search for specific Plex Episode
logger.warning("Retry using search for specific Plex Episode")
if not pe.is_episode:
return self.find_by_media(pe)
return None

def flush(self):
Expand All @@ -234,24 +238,42 @@ def flush(self):


class TraktBatch:
def __init__(self, trakt: TraktApi):
def __init__(self, trakt: TraktApi, batch_size=None):
self.trakt = trakt
self.batch_size = batch_size
self.collection = {}

@nocache
@rate_limit(delay=TRAKT_POST_DELAY)
def submit_collection(self):
if not len(self.collection):
return None
if self.queue_size() == 0:
return

try:
result = trakt.sync.add_to_collection(self.collection)
result = self.remove_empty_values(result)
result = self.trakt_sync_collection(self.collection)
result = self.remove_empty_values(result.copy())
if result:
logger.info(f"Updated Trakt collection: {result}")
finally:
self.collection.clear()

def queue_size(self):
size = 0
for media_type in self.collection:
size += len(self.collection[media_type])

return size

def flush(self):
"""
Flush the queue if it's bigger than batch_size
"""
if not self.batch_size:
return

if self.queue_size() >= self.batch_size:
self.submit_collection()

def add_to_collection(self, media_type: str, item):
"""
Add item of media_type to collection
Expand All @@ -260,6 +282,10 @@ def add_to_collection(self, media_type: str, item):
self.collection[media_type] = []

self.collection[media_type].append(item)
self.flush()

def trakt_sync_collection(self, media_object):
return trakt.sync.add_to_collection(media_object)

def remove_empty_values(self, result):
"""
Expand Down
11 changes: 11 additions & 0 deletions plex_trakt_sync/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
def git_version_info():
try:
from gitinfo import get_git_info
except ImportError:
return

commit = get_git_info()
if not commit:
return None

return f"{commit['commit'][0:8]}: {commit['message']} @{commit['author_date']}"
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
click==7.1.2
plexapi==4.5.0
python-dotenv==0.15.0
python-git-info==0.6.1
requests-cache==0.5.2
trakt==3.1.0
Loading

0 comments on commit 0f37555

Please sign in to comment.