Skip to content

Commit

Permalink
Merge pull request #44 from luigi311/dev
Browse files Browse the repository at this point in the history
Fix issues with certain libraries failing
  • Loading branch information
luigi311 authored Feb 26, 2023
2 parents d1fd61f + b960bcc commit a4365e5
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 77 deletions.
16 changes: 8 additions & 8 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ SLEEP_DURATION = "3600"
LOGFILE = "log.log"

## Map usernames between servers in the event that they are different, order does not matter
## Comma seperated for multiple options
## Comma separated for multiple options
#USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" }

## Map libraries between servers in the even that they are different, order does not matter
## Comma seperated for multiple options
## Comma separated for multiple options
#LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" }

## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded.
## Comma seperated for multiple options
## Comma separated for multiple options
#BLACKLIST_LIBRARY = ""
#WHITELIST_LIBRARY = ""
#BLACKLIST_LIBRARY_TYPE = ""
Expand All @@ -38,15 +38,15 @@ WHITELIST_USERS = "testuser1,testuser2"

## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
## Comma separated list for multiple servers
PLEX_BASEURL = "http://localhost:32400, https://nas:32400"

## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
## Comma seperated list for multiple servers
## Comma separated list for multiple servers
PLEX_TOKEN = "SuperSecretToken, SuperSecretToken2"

## If not using plex token then use username and password of the server admin along with the servername
## Comma seperated for multiple options
## Comma separated for multiple options
#PLEX_USERNAME = "PlexUser, PlexUser2"
#PLEX_PASSWORD = "SuperSecret, SuperSecret2"
#PLEX_SERVERNAME = "Plex Server1, Plex Server2"
Expand All @@ -60,9 +60,9 @@ SSL_BYPASS = "False"
# Jellyfin

## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
## Comma separated list for multiple servers
JELLYFIN_BASEURL = "http://localhost:8096, http://nas:8096"

## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
## Comma seperated list for multiple servers
## Comma separated list for multiple servers
JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2"
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Sync watched between jellyfin and plex locally

## Description

Keep in sync all your users watched history between jellyfin and plex servers locally. This uses file names and provider ids to find the correct episode/movie between the two. This is not perfect but it works for most cases. You can use this for as many servers as you want by enterying multiple options in the .env plex/jellyfin section seperated by commas.
Keep in sync all your users watched history between jellyfin and plex servers locally. This uses file names and provider ids to find the correct episode/movie between the two. This is not perfect but it works for most cases. You can use this for as many servers as you want by entering multiple options in the .env plex/jellyfin section separated by commas.

## Configuration

Expand All @@ -29,15 +29,15 @@ SLEEP_DURATION = "3600"
LOGFILE = "log.log"

## Map usernames between servers in the event that they are different, order does not matter
## Comma seperated for multiple options
## Comma separated for multiple options
USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" }

## Map libraries between servers in the even that they are different, order does not matter
## Comma seperated for multiple options
## Comma separated for multiple options
LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" }

## Blacklisting/Whitelisting libraries, library types such as Movies/TV Shows, and users. Mappings apply so if the mapping for the user or library exist then both will be excluded.
## Comma seperated for multiple options
## Comma separated for multiple options
BLACKLIST_LIBRARY = ""
WHITELIST_LIBRARY = ""
BLACKLIST_LIBRARY_TYPE = ""
Expand All @@ -51,15 +51,15 @@ WHITELIST_USERS = "testuser1,testuser2"

## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
## Comma separated list for multiple servers
PLEX_BASEURL = "http://localhost:32400, https://nas:32400"

## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
## Comma seperated list for multiple servers
## Comma separated list for multiple servers
PLEX_TOKEN = "SuperSecretToken, SuperSecretToken2"

## If not using plex token then use username and password of the server admin along with the servername
## Comma seperated for multiple options
## Comma separated for multiple options
#PLEX_USERNAME = "PlexUser, PlexUser2"
#PLEX_PASSWORD = "SuperSecret, SuperSecret2"
#PLEX_SERVERNAME = "Plex Server1, Plex Server2"
Expand All @@ -73,11 +73,11 @@ SSL_BYPASS = "False"
# Jellyfin

## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
## Comma seperated list for multiple servers
## Comma separated list for multiple servers
JELLYFIN_BASEURL = "http://localhost:8096, http://nas:8096"

## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
## Comma seperated list for multiple servers
## Comma separated list for multiple servers
JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2"
```
Expand Down Expand Up @@ -136,7 +136,7 @@ JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2"
## Contributing
I am open to recieving pull requests. If you are submitting a pull request, please make sure run it locally for a day or two to make sure it is working as expected and stable. Make all pull requests against the dev branch and nothing will be merged into the main without going through the lower branches.
I am open to receiving pull requests. If you are submitting a pull request, please make sure run it locally for a day or two to make sure it is working as expected and stable. Make all pull requests against the dev branch and nothing will be merged into the main without going through the lower branches.
## License
Expand Down
26 changes: 20 additions & 6 deletions src/jellyfin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import asyncio, aiohttp
import asyncio, aiohttp, traceback
from src.functions import (
logger,
search_mapping,
Expand Down Expand Up @@ -46,9 +46,14 @@ async def query(self, query, query_type, session, identifiers=None):
) as response:
results = await response.json()

if type(results) is str:
logger(f"Jellyfin: Query {query_type} {query} {results}", 2)
raise Exception(results)

# append identifiers to results
if identifiers:
results["Identifiers"] = identifiers

return results

except Exception as e:
Expand All @@ -63,7 +68,7 @@ async def get_users(self):
async with aiohttp.ClientSession() as session:
response = await self.query(query_string, "get", session)

# If reponse is not empty
# If response is not empty
if response:
for user in response:
users[user["Name"]] = user["Id"]
Expand Down Expand Up @@ -148,7 +153,7 @@ async def get_user_library_watched(
)

# TV Shows
if library_type == "Series":
if library_type in ["Series", "Episode"]:
# Initialize an empty dictionary for the given user and library
user_watched[user_name][library_title] = {}

Expand Down Expand Up @@ -184,6 +189,7 @@ async def get_user_library_watched(
"show_guids": show_guids,
"show_id": show["Id"],
}

season_task = asyncio.ensure_future(
self.query(
f"/Shows/{show['Id']}/Seasons"
Expand Down Expand Up @@ -309,7 +315,9 @@ async def get_user_library_watched(
f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}",
2,
)
raise Exception(e)

logger(traceback.format_exc(), 2)
return {}

async def get_users_watched(
self,
Expand Down Expand Up @@ -362,7 +370,7 @@ async def get_users_watched(
[
x["Type"]
for x in watched["Items"]
if x["Type"] in ["Movie", "Series"]
if x["Type"] in ["Movie", "Series", "Episode"]
]
)

Expand All @@ -385,8 +393,14 @@ async def get_users_watched(

# If there are multiple types in library raise error
if types is None or len(types) < 1:
all_types = set(
[
x["Type"]
for x in watched["Items"]
]
)
logger(
f"Jellyfin: Skipping Library {library_title} not a single type: {types}",
f"Jellyfin: Skipping Library {library_title} found types: {types}, all types: {all_types}",
1,
)
continue
Expand Down
134 changes: 81 additions & 53 deletions src/plex.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import re, requests
import re, requests, os, traceback
from urllib3.poolmanager import PoolManager

from plexapi.server import PlexServer
Expand All @@ -24,6 +24,52 @@ def init_poolmanager(self, connections, maxsize, block=..., **pool_kwargs):
)


def get_user_library_watched_show(show):
try:
show_guids = {}
for show_guid in show.guids:
# Extract source and id from guid.id
m = re.match(r"(.*)://(.*)", show_guid.id)
show_guid_source, show_guid_id = m.group(1).lower(), m.group(2)
show_guids[show_guid_source] = show_guid_id

show_guids["title"] = show.title
show_guids["locations"] = tuple([x.split("/")[-1] for x in show.locations])
show_guids = frozenset(show_guids.items())

# Get all watched episodes for show
episode_guids = {}
watched_episodes = show.watched()
for episode in watched_episodes:
episode_guids_temp = {}
try:
if len(episode.guids) > 0:
for guid in episode.guids:
# Extract after :// from guid.id
m = re.match(r"(.*)://(.*)", guid.id)
guid_source, guid_id = m.group(1).lower(), m.group(2)
episode_guids_temp[guid_source] = guid_id
except:
logger(
f"Plex: Failed to get guids for {episode.title} in {show.title}, Using location only",
1,
)

episode_guids_temp["locations"] = tuple(
[x.split("/")[-1] for x in episode.locations]
)

if episode.parentTitle not in episode_guids:
episode_guids[episode.parentTitle] = []

episode_guids[episode.parentTitle].append(episode_guids_temp)

return show_guids, episode_guids

except Exception as e:
return {}, {}


def get_user_library_watched(user, user_plex, library):
try:
user_name = user.title.lower()
Expand Down Expand Up @@ -61,41 +107,17 @@ def get_user_library_watched(user, user_plex, library):

elif library.type == "show":
user_watched[user_name][library.title] = {}
shows = library_videos.search(unwatched=False)

for show in library_videos.search(unwatched=False):
logger(f"Plex: Adding {show.title} to {user_name} watched list", 3)
show_guids = {}
for show_guid in show.guids:
# Extract source and id from guid.id
m = re.match(r"(.*)://(.*)", show_guid.id)
show_guid_source, show_guid_id = m.group(1).lower(), m.group(2)
show_guids[show_guid_source] = show_guid_id

show_guids["title"] = show.title
show_guids["locations"] = tuple(
[x.split("/")[-1] for x in show.locations]
)
show_guids = frozenset(show_guids.items())

# Get all watched episodes for show
episode_guids = {}
for episode in show.watched():
if episode.viewCount > 0:
episode_guids_temp = {}
for guid in episode.guids:
# Extract after :// from guid.id
m = re.match(r"(.*)://(.*)", guid.id)
guid_source, guid_id = m.group(1).lower(), m.group(2)
episode_guids_temp[guid_source] = guid_id

episode_guids_temp["locations"] = tuple(
[x.split("/")[-1] for x in episode.locations]
)
if episode.parentTitle not in episode_guids:
episode_guids[episode.parentTitle] = []
episode_guids[episode.parentTitle].append(episode_guids_temp)
# Parallelize show processing
args = []
for show in shows:
args.append([get_user_library_watched_show, show])

if episode_guids:
for show_guids, episode_guids in future_thread_executor(
args, workers=min(os.cpu_count(), 4)
):
if show_guids and episode_guids:
# append show, season, episode
if show_guids not in user_watched[user_name][library.title]:
user_watched[user_name][library.title][show_guids] = {}
Expand All @@ -116,7 +138,7 @@ def get_user_library_watched(user, user_plex, library):
f"Plex: Failed to get watched for {user_name} in library {library.title}, Error: {e}",
2,
)
raise Exception(e)
return {}


def update_user_watched(user, user_plex, library, videos, dryrun):
Expand Down Expand Up @@ -201,24 +223,30 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
break

if not episode_found:
for episode_guid in episode_search.guids:
episode_guid_source = (
re.search(r"(.*)://", episode_guid.id)
.group(1)
.lower()
try:
for episode_guid in episode_search.guids:
episode_guid_source = (
re.search(r"(.*)://", episode_guid.id)
.group(1)
.lower()
)
episode_guid_id = re.search(
r"://(.*)", episode_guid.id
).group(1)

# If episode provider source and episode provider id are in videos_episodes_ids exactly, then the episode is in the list
if episode_guid_source in videos_episodes_ids.keys():
if (
episode_guid_id
in videos_episodes_ids[episode_guid_source]
):
episode_found = True
break
except Exception as e:
logger(
f"Plex: Failed to get episode guid for {episode_search.title}, Error: {e}",
1,
)
episode_guid_id = re.search(
r"://(.*)", episode_guid.id
).group(1)

# If episode provider source and episode provider id are in videos_episodes_ids exactly, then the episode is in the list
if episode_guid_source in videos_episodes_ids.keys():
if (
episode_guid_id
in videos_episodes_ids[episode_guid_source]
):
episode_found = True
break

if episode_found:
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex"
Expand Down Expand Up @@ -249,7 +277,7 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}",
2,
)
raise Exception(e)
logger(traceback.format_exc(), 2)


# class plex accept base url and token and username and password but default with none
Expand Down

0 comments on commit a4365e5

Please sign in to comment.