Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4365e59f3 | ||
|
|
b960bccb86 | ||
|
|
218037200c | ||
|
|
4ac670e837 | ||
|
|
96eff65c3e | ||
|
|
45471607c8 | ||
|
|
14885744b1 | ||
|
|
d1fd61f1d1 | ||
|
|
6c1ee4a7dc | ||
|
|
9a8e799e68 | ||
|
|
ffec4e2f28 | ||
|
|
00102891a5 | ||
|
|
aa76b83428 | ||
|
|
a644189ea5 | ||
|
|
c5d987a8c9 | ||
|
|
bdd68ad68d | ||
|
|
2d86bca781 | ||
|
|
1b01ff6ec2 | ||
|
|
f08ec43507 | ||
|
|
7f9424260a | ||
|
|
5f21943353 | ||
|
|
a5a795f43c | ||
|
|
fcb6d7625f | ||
|
|
fd2179998f |
73
.env.sample
73
.env.sample
@@ -1,43 +1,68 @@
|
|||||||
|
# Global Settings
|
||||||
|
|
||||||
## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
|
## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
|
||||||
DRYRUN = "True"
|
DRYRUN = "True"
|
||||||
|
|
||||||
## Additional logging information
|
## Additional logging information
|
||||||
DEBUG = "True"
|
DEBUG = "False"
|
||||||
|
|
||||||
## Debugging level, "info" is default, "debug" is more verbose
|
## Debugging level, "info" is default, "debug" is more verbose
|
||||||
DEBUG_LEVEL = "info"
|
DEBUG_LEVEL = "info"
|
||||||
|
|
||||||
## How often to run the script in seconds
|
## How often to run the script in seconds
|
||||||
SLEEP_DURATION = "3600"
|
SLEEP_DURATION = "3600"
|
||||||
|
|
||||||
## Log file where all output will be written to
|
## Log file where all output will be written to
|
||||||
LOGFILE = "log.log"
|
LOGFILE = "log.log"
|
||||||
## Map usernames between plex and jellyfin in the event that they are different, order does not matter
|
|
||||||
#USER_MAPPING = { "testuser2": "testuser3" }
|
|
||||||
## Map libraries between plex and jellyfin in the even that they are different, order does not matter
|
|
||||||
#LIBRARY_MAPPING = { "Shows": "TV Shows" }
|
|
||||||
|
|
||||||
|
## Map usernames between servers in the event that they are different, order does not matter
|
||||||
|
## Comma separated for multiple options
|
||||||
|
#USER_MAPPING = { "testuser2": "testuser3", "testuser1":"testuser4" }
|
||||||
|
|
||||||
## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers
|
## Map libraries between servers in the even that they are different, order does not matter
|
||||||
## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly
|
## Comma separated for multiple options
|
||||||
## Comma seperated list for multiple servers
|
#LIBRARY_MAPPING = { "Shows": "TV Shows", "Movie": "Movies" }
|
||||||
PLEX_BASEURL = "http://localhost:32400"
|
|
||||||
## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
|
|
||||||
PLEX_TOKEN = "SuperSecretToken"
|
|
||||||
## If not using plex token then use username and password of the server admin along with the servername
|
|
||||||
#PLEX_USERNAME = ""
|
|
||||||
#PLEX_PASSWORD = ""
|
|
||||||
#PLEX_SERVERNAME = "Plex Server"
|
|
||||||
## Skip hostname validation for ssl certificates.
|
|
||||||
SSL_BYPASS = "False"
|
|
||||||
|
|
||||||
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
|
|
||||||
## Comma seperated list for multiple servers
|
|
||||||
JELLYFIN_BASEURL = "http://localhost:8096"
|
|
||||||
## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key
|
|
||||||
JELLYFIN_TOKEN = "SuperSecretToken"
|
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
## 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 separated for multiple options
|
||||||
#BLACKLIST_LIBRARY = ""
|
#BLACKLIST_LIBRARY = ""
|
||||||
#WHITELIST_LIBRARY = ""
|
#WHITELIST_LIBRARY = ""
|
||||||
#BLACKLIST_LIBRARY_TYPE = ""
|
#BLACKLIST_LIBRARY_TYPE = ""
|
||||||
#WHITELIST_LIBRARY_TYPE = ""
|
#WHITELIST_LIBRARY_TYPE = ""
|
||||||
#BLACKLIST_USERS = ""
|
#BLACKLIST_USERS = ""
|
||||||
WHITELIST_USERS = "testuser1,testuser2"
|
WHITELIST_USERS = "testuser1,testuser2"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Plex
|
||||||
|
|
||||||
|
## 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 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 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 separated for multiple options
|
||||||
|
#PLEX_USERNAME = "PlexUser, PlexUser2"
|
||||||
|
#PLEX_PASSWORD = "SuperSecret, SuperSecret2"
|
||||||
|
#PLEX_SERVERNAME = "Plex Server1, Plex Server2"
|
||||||
|
|
||||||
|
## Skip hostname validation for ssl certificates.
|
||||||
|
## Set to True if running into ssl certificate errors
|
||||||
|
SSL_BYPASS = "False"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Jellyfin
|
||||||
|
|
||||||
|
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
|
||||||
|
## 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 separated list for multiple servers
|
||||||
|
JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2"
|
||||||
|
|||||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
pytest:
|
pytest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: "Install dependencies"
|
||||||
run: pip install -r requirements.txt && pip install -r test/requirements.txt
|
run: pip install -r requirements.txt && pip install -r test/requirements.txt
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
needs: pytest
|
needs: pytest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
@@ -45,14 +45,14 @@ jobs:
|
|||||||
type=sha
|
type=sha
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: "${{ steps.docker_meta.outcome == 'success' }}"
|
if: "${{ steps.docker_meta.outcome == 'success' }}"
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
id: build
|
id: build
|
||||||
if: "${{ steps.docker_meta.outcome == 'skipped' }}"
|
if: "${{ steps.docker_meta.outcome == 'skipped' }}"
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
- name: Build Push
|
- name: Build Push
|
||||||
id: build_push
|
id: build_push
|
||||||
if: "${{ steps.docker_meta.outcome == 'success' }}"
|
if: "${{ steps.docker_meta.outcome == 'success' }}"
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"python.formatting.provider": "black"
|
||||||
|
}
|
||||||
76
README.md
76
README.md
@@ -2,14 +2,84 @@
|
|||||||
|
|
||||||
[](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com&utm_medium=referral&utm_content=luigi311/JellyPlex-Watched&utm_campaign=Badge_Grade)
|
[](https://www.codacy.com/gh/luigi311/JellyPlex-Watched/dashboard?utm_source=github.com&utm_medium=referral&utm_content=luigi311/JellyPlex-Watched&utm_campaign=Badge_Grade)
|
||||||
|
|
||||||
Sync watched between jellyfin and plex
|
Sync watched between jellyfin and plex locally
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Keep in sync all your users watched history between jellyfin and plex servers locally. This uses the imdb ids and any other matching id 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
|
## Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Global Settings
|
||||||
|
|
||||||
|
## Do not mark any shows/movies as played and instead just output to log if they would of been marked.
|
||||||
|
DRYRUN = "True"
|
||||||
|
|
||||||
|
## Additional logging information
|
||||||
|
DEBUG = "False"
|
||||||
|
|
||||||
|
## Debugging level, "info" is default, "debug" is more verbose
|
||||||
|
DEBUG_LEVEL = "info"
|
||||||
|
|
||||||
|
## How often to run the script in seconds
|
||||||
|
SLEEP_DURATION = "3600"
|
||||||
|
|
||||||
|
## Log file where all output will be written to
|
||||||
|
LOGFILE = "log.log"
|
||||||
|
|
||||||
|
## Map usernames between servers in the event that they are different, order does not matter
|
||||||
|
## 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 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 separated for multiple options
|
||||||
|
BLACKLIST_LIBRARY = ""
|
||||||
|
WHITELIST_LIBRARY = ""
|
||||||
|
BLACKLIST_LIBRARY_TYPE = ""
|
||||||
|
WHITELIST_LIBRARY_TYPE = ""
|
||||||
|
BLACKLIST_USERS = ""
|
||||||
|
WHITELIST_USERS = "testuser1,testuser2"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Plex
|
||||||
|
|
||||||
|
## 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 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 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 separated for multiple options
|
||||||
|
#PLEX_USERNAME = "PlexUser, PlexUser2"
|
||||||
|
#PLEX_PASSWORD = "SuperSecret, SuperSecret2"
|
||||||
|
#PLEX_SERVERNAME = "Plex Server1, Plex Server2"
|
||||||
|
|
||||||
|
## Skip hostname validation for ssl certificates.
|
||||||
|
## Set to True if running into ssl certificate errors
|
||||||
|
SSL_BYPASS = "False"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Jellyfin
|
||||||
|
|
||||||
|
## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly
|
||||||
|
## 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 separated list for multiple servers
|
||||||
|
JELLYFIN_TOKEN = "SuperSecretToken, SuperSecretToken2"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -66,7 +136,7 @@ Keep in sync all your users watched history between jellyfin and plex servers lo
|
|||||||
|
|
||||||
## Contributing
|
## 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
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -168,8 +168,13 @@ def check_skip_logic(
|
|||||||
):
|
):
|
||||||
skip_reason = None
|
skip_reason = None
|
||||||
|
|
||||||
if library_type.lower() in blacklist_library_type:
|
if isinstance(library_type, (list, tuple, set)):
|
||||||
skip_reason = "is blacklist_library_type"
|
for library_type_item in library_type:
|
||||||
|
if library_type_item.lower() in blacklist_library_type:
|
||||||
|
skip_reason = "is blacklist_library_type"
|
||||||
|
else:
|
||||||
|
if library_type.lower() in blacklist_library_type:
|
||||||
|
skip_reason = "is blacklist_library_type"
|
||||||
|
|
||||||
if library_title.lower() in [x.lower() for x in blacklist_library]:
|
if library_title.lower() in [x.lower() for x in blacklist_library]:
|
||||||
skip_reason = "is blacklist_library"
|
skip_reason = "is blacklist_library"
|
||||||
@@ -182,8 +187,13 @@ def check_skip_logic(
|
|||||||
skip_reason = "is blacklist_library"
|
skip_reason = "is blacklist_library"
|
||||||
|
|
||||||
if len(whitelist_library_type) > 0:
|
if len(whitelist_library_type) > 0:
|
||||||
if library_type.lower() not in whitelist_library_type:
|
if isinstance(library_type, (list, tuple, set)):
|
||||||
skip_reason = "is not whitelist_library_type"
|
for library_type_item in library_type:
|
||||||
|
if library_type_item.lower() not in whitelist_library_type:
|
||||||
|
skip_reason = "is not whitelist_library_type"
|
||||||
|
else:
|
||||||
|
if library_type.lower() not in whitelist_library_type:
|
||||||
|
skip_reason = "is not whitelist_library_type"
|
||||||
|
|
||||||
# if whitelist is not empty and library is not in whitelist
|
# if whitelist is not empty and library is not in whitelist
|
||||||
if len(whitelist_library) > 0:
|
if len(whitelist_library) > 0:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import asyncio, aiohttp
|
import asyncio, aiohttp, traceback
|
||||||
from src.functions import (
|
from src.functions import (
|
||||||
logger,
|
logger,
|
||||||
search_mapping,
|
search_mapping,
|
||||||
@@ -46,9 +46,14 @@ class Jellyfin:
|
|||||||
) as response:
|
) as response:
|
||||||
results = await response.json()
|
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
|
# append identifiers to results
|
||||||
if identifiers:
|
if identifiers:
|
||||||
results["Identifiers"] = identifiers
|
results["Identifiers"] = identifiers
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -63,7 +68,7 @@ class Jellyfin:
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await self.query(query_string, "get", session)
|
response = await self.query(query_string, "get", session)
|
||||||
|
|
||||||
# If reponse is not empty
|
# If response is not empty
|
||||||
if response:
|
if response:
|
||||||
for user in response:
|
for user in response:
|
||||||
users[user["Name"]] = user["Id"]
|
users[user["Name"]] = user["Id"]
|
||||||
@@ -92,7 +97,7 @@ class Jellyfin:
|
|||||||
user_watched[user_name][library_title] = []
|
user_watched[user_name][library_title] = []
|
||||||
watched = await self.query(
|
watched = await self.query(
|
||||||
f"/Users/{user_id}/Items"
|
f"/Users/{user_id}/Items"
|
||||||
+ f"?ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds,MediaSources",
|
+ f"?ParentId={library_id}&Filters=IsPlayed&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources",
|
||||||
"get",
|
"get",
|
||||||
session,
|
session,
|
||||||
)
|
)
|
||||||
@@ -148,14 +153,14 @@ class Jellyfin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# TV Shows
|
# TV Shows
|
||||||
if library_type == "Series":
|
if library_type in ["Series", "Episode"]:
|
||||||
# Initialize an empty dictionary for the given user and library
|
# Initialize an empty dictionary for the given user and library
|
||||||
user_watched[user_name][library_title] = {}
|
user_watched[user_name][library_title] = {}
|
||||||
|
|
||||||
# Retrieve a list of watched TV shows
|
# Retrieve a list of watched TV shows
|
||||||
watched_shows = await self.query(
|
watched_shows = await self.query(
|
||||||
f"/Users/{user_id}/Items"
|
f"/Users/{user_id}/Items"
|
||||||
+ f"?ParentId={library_id}&isPlaceHolder=false&Fields=ProviderIds,Path,RecursiveItemCount",
|
+ f"?ParentId={library_id}&isPlaceHolder=false&IncludeItemTypes=Series&Recursive=True&Fields=ProviderIds,Path,RecursiveItemCount",
|
||||||
"get",
|
"get",
|
||||||
session,
|
session,
|
||||||
)
|
)
|
||||||
@@ -184,6 +189,7 @@ class Jellyfin:
|
|||||||
"show_guids": show_guids,
|
"show_guids": show_guids,
|
||||||
"show_id": show["Id"],
|
"show_id": show["Id"],
|
||||||
}
|
}
|
||||||
|
|
||||||
season_task = asyncio.ensure_future(
|
season_task = asyncio.ensure_future(
|
||||||
self.query(
|
self.query(
|
||||||
f"/Shows/{show['Id']}/Seasons"
|
f"/Shows/{show['Id']}/Seasons"
|
||||||
@@ -309,7 +315,9 @@ class Jellyfin:
|
|||||||
f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}",
|
f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}",
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
raise Exception(e)
|
|
||||||
|
logger(traceback.format_exc(), 2)
|
||||||
|
return {}
|
||||||
|
|
||||||
async def get_users_watched(
|
async def get_users_watched(
|
||||||
self,
|
self,
|
||||||
@@ -339,7 +347,7 @@ class Jellyfin:
|
|||||||
task = asyncio.ensure_future(
|
task = asyncio.ensure_future(
|
||||||
self.query(
|
self.query(
|
||||||
f"/Users/{user_id}/Items"
|
f"/Users/{user_id}/Items"
|
||||||
+ f"?ParentId={library_id}&Filters=IsPlayed&limit=1",
|
+ f"?ParentId={library_id}&Filters=IsPlayed&Recursive=True&excludeItemTypes=Folder&limit=100",
|
||||||
"get",
|
"get",
|
||||||
session,
|
session,
|
||||||
identifiers=identifiers,
|
identifiers=identifiers,
|
||||||
@@ -357,11 +365,18 @@ class Jellyfin:
|
|||||||
|
|
||||||
library_id = watched["Identifiers"]["library_id"]
|
library_id = watched["Identifiers"]["library_id"]
|
||||||
library_title = watched["Identifiers"]["library_title"]
|
library_title = watched["Identifiers"]["library_title"]
|
||||||
library_type = watched["Items"][0]["Type"]
|
# Get all library types excluding "Folder"
|
||||||
|
types = set(
|
||||||
|
[
|
||||||
|
x["Type"]
|
||||||
|
for x in watched["Items"]
|
||||||
|
if x["Type"] in ["Movie", "Series", "Episode"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
skip_reason = check_skip_logic(
|
skip_reason = check_skip_logic(
|
||||||
library_title,
|
library_title,
|
||||||
library_type,
|
types,
|
||||||
blacklist_library,
|
blacklist_library,
|
||||||
whitelist_library,
|
whitelist_library,
|
||||||
blacklist_library_type,
|
blacklist_library_type,
|
||||||
@@ -376,15 +391,35 @@ class Jellyfin:
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get watched for user
|
# If there are multiple types in library raise error
|
||||||
task = asyncio.ensure_future(
|
if types is None or len(types) < 1:
|
||||||
self.get_user_library_watched(
|
all_types = set(
|
||||||
user_name, user_id, library_type, library_id, library_title
|
[
|
||||||
|
x["Type"]
|
||||||
|
for x in watched["Items"]
|
||||||
|
]
|
||||||
)
|
)
|
||||||
)
|
logger(
|
||||||
tasks_watched.append(task)
|
f"Jellyfin: Skipping Library {library_title} found types: {types}, all types: {all_types}",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for library_type in types:
|
||||||
|
# Get watched for user
|
||||||
|
task = asyncio.ensure_future(
|
||||||
|
self.get_user_library_watched(
|
||||||
|
user_name,
|
||||||
|
user_id,
|
||||||
|
library_type,
|
||||||
|
library_id,
|
||||||
|
library_title,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tasks_watched.append(task)
|
||||||
|
|
||||||
watched = await asyncio.gather(*tasks_watched, return_exceptions=True)
|
watched = await asyncio.gather(*tasks_watched, return_exceptions=True)
|
||||||
|
|
||||||
return watched
|
return watched
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2)
|
logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2)
|
||||||
@@ -450,8 +485,8 @@ class Jellyfin:
|
|||||||
if videos_movies_ids:
|
if videos_movies_ids:
|
||||||
jellyfin_search = await self.query(
|
jellyfin_search = await self.query(
|
||||||
f"/Users/{user_id}/Items"
|
f"/Users/{user_id}/Items"
|
||||||
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}"
|
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}"
|
||||||
+ "&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources",
|
+ "&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources&IncludeItemTypes=Movie",
|
||||||
"get",
|
"get",
|
||||||
session,
|
session,
|
||||||
)
|
)
|
||||||
@@ -504,8 +539,8 @@ class Jellyfin:
|
|||||||
if videos_shows_ids and videos_episodes_ids:
|
if videos_shows_ids and videos_episodes_ids:
|
||||||
jellyfin_search = await self.query(
|
jellyfin_search = await self.query(
|
||||||
f"/Users/{user_id}/Items"
|
f"/Users/{user_id}/Items"
|
||||||
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}"
|
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}"
|
||||||
+ "&isPlayed=false&Fields=ItemCounts,ProviderIds,Path",
|
+ "&isPlayed=false&Fields=ItemCounts,ProviderIds,Path&IncludeItemTypes=Series",
|
||||||
"get",
|
"get",
|
||||||
session,
|
session,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -113,12 +113,12 @@ def setup_users(
|
|||||||
|
|
||||||
if len(output_server_1_users) == 0:
|
if len(output_server_1_users) == 0:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"No users found for server 1, users found {users} filtered users {users_filtered}"
|
f"No users found for server 1 {server_1_type}, users found {users}, filtered users {users_filtered}, server 1 users {server_1_connection.users}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(output_server_2_users) == 0:
|
if len(output_server_2_users) == 0:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"No users found for server 2, users found {users} filtered users {users_filtered}"
|
f"No users found for server 2 {server_2_type}, users found {users} filtered users {users_filtered}, server 2 users {server_2_connection.users}"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger(f"Server 1 users: {output_server_1_users}", 1)
|
logger(f"Server 1 users: {output_server_1_users}", 1)
|
||||||
|
|||||||
166
src/plex.py
166
src/plex.py
@@ -1,4 +1,4 @@
|
|||||||
import re, requests
|
import re, requests, os, traceback
|
||||||
from urllib3.poolmanager import PoolManager
|
from urllib3.poolmanager import PoolManager
|
||||||
|
|
||||||
from plexapi.server import PlexServer
|
from plexapi.server import PlexServer
|
||||||
@@ -24,6 +24,52 @@ class HostNameIgnoringAdapter(requests.adapters.HTTPAdapter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def get_user_library_watched(user, user_plex, library):
|
||||||
try:
|
try:
|
||||||
user_name = user.title.lower()
|
user_name = user.title.lower()
|
||||||
@@ -61,41 +107,17 @@ def get_user_library_watched(user, user_plex, library):
|
|||||||
|
|
||||||
elif library.type == "show":
|
elif library.type == "show":
|
||||||
user_watched[user_name][library.title] = {}
|
user_watched[user_name][library.title] = {}
|
||||||
|
shows = library_videos.search(unwatched=False)
|
||||||
|
|
||||||
for show in library_videos.search(unwatched=False):
|
# Parallelize show processing
|
||||||
logger(f"Plex: Adding {show.title} to {user_name} watched list", 3)
|
args = []
|
||||||
show_guids = {}
|
for show in shows:
|
||||||
for show_guid in show.guids:
|
args.append([get_user_library_watched_show, show])
|
||||||
# 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
|
for show_guids, episode_guids in future_thread_executor(
|
||||||
show_guids["locations"] = tuple(
|
args, workers=min(os.cpu_count(), 4)
|
||||||
[x.split("/")[-1] for x in show.locations]
|
):
|
||||||
)
|
if show_guids and episode_guids:
|
||||||
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)
|
|
||||||
|
|
||||||
if episode_guids:
|
|
||||||
# append show, season, episode
|
# append show, season, episode
|
||||||
if show_guids not in user_watched[user_name][library.title]:
|
if show_guids not in user_watched[user_name][library.title]:
|
||||||
user_watched[user_name][library.title][show_guids] = {}
|
user_watched[user_name][library.title][show_guids] = {}
|
||||||
@@ -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}",
|
f"Plex: Failed to get watched for {user_name} in library {library.title}, Error: {e}",
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
raise Exception(e)
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def update_user_watched(user, user_plex, library, videos, dryrun):
|
def update_user_watched(user, user_plex, library, videos, dryrun):
|
||||||
@@ -201,24 +223,30 @@ def update_user_watched(user, user_plex, library, videos, dryrun):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not episode_found:
|
if not episode_found:
|
||||||
for episode_guid in episode_search.guids:
|
try:
|
||||||
episode_guid_source = (
|
for episode_guid in episode_search.guids:
|
||||||
re.search(r"(.*)://", episode_guid.id)
|
episode_guid_source = (
|
||||||
.group(1)
|
re.search(r"(.*)://", episode_guid.id)
|
||||||
.lower()
|
.group(1)
|
||||||
)
|
.lower()
|
||||||
episode_guid_id = re.search(
|
)
|
||||||
r"://(.*)", episode_guid.id
|
episode_guid_id = re.search(
|
||||||
).group(1)
|
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 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_source in videos_episodes_ids.keys():
|
||||||
if (
|
if (
|
||||||
episode_guid_id
|
episode_guid_id
|
||||||
in videos_episodes_ids[episode_guid_source]
|
in videos_episodes_ids[episode_guid_source]
|
||||||
):
|
):
|
||||||
episode_found = True
|
episode_found = True
|
||||||
break
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger(
|
||||||
|
f"Plex: Failed to get episode guid for {episode_search.title}, Error: {e}",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
if episode_found:
|
if episode_found:
|
||||||
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex"
|
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex"
|
||||||
@@ -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}",
|
f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}",
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
raise Exception(e)
|
logger(traceback.format_exc(), 2)
|
||||||
|
|
||||||
|
|
||||||
# class plex accept base url and token and username and password but default with none
|
# class plex accept base url and token and username and password but default with none
|
||||||
@@ -262,6 +290,7 @@ class Plex:
|
|||||||
password=None,
|
password=None,
|
||||||
servername=None,
|
servername=None,
|
||||||
ssl_bypass=False,
|
ssl_bypass=False,
|
||||||
|
session=None,
|
||||||
):
|
):
|
||||||
self.baseurl = baseurl
|
self.baseurl = baseurl
|
||||||
self.token = token
|
self.token = token
|
||||||
@@ -269,21 +298,20 @@ class Plex:
|
|||||||
self.password = password
|
self.password = password
|
||||||
self.servername = servername
|
self.servername = servername
|
||||||
self.ssl_bypass = ssl_bypass
|
self.ssl_bypass = ssl_bypass
|
||||||
self.plex = self.login(self.baseurl, self.token, ssl_bypass)
|
if ssl_bypass:
|
||||||
|
# Session for ssl bypass
|
||||||
|
session = requests.Session()
|
||||||
|
# By pass ssl hostname check https://github.com/pkkid/python-plexapi/issues/143#issuecomment-775485186
|
||||||
|
session.mount("https://", HostNameIgnoringAdapter())
|
||||||
|
self.session = session
|
||||||
|
self.plex = self.login(self.baseurl, self.token)
|
||||||
self.admin_user = self.plex.myPlexAccount()
|
self.admin_user = self.plex.myPlexAccount()
|
||||||
self.users = self.get_users()
|
self.users = self.get_users()
|
||||||
|
|
||||||
def login(self, baseurl, token, ssl_bypass=False):
|
def login(self, baseurl, token):
|
||||||
try:
|
try:
|
||||||
if baseurl and token:
|
if baseurl and token:
|
||||||
# Login via token
|
plex = PlexServer(baseurl, token, session=self.session)
|
||||||
if ssl_bypass:
|
|
||||||
session = requests.Session()
|
|
||||||
# By pass ssl hostname check https://github.com/pkkid/python-plexapi/issues/143#issuecomment-775485186
|
|
||||||
session.mount("https://", HostNameIgnoringAdapter())
|
|
||||||
plex = PlexServer(baseurl, token, session=session)
|
|
||||||
else:
|
|
||||||
plex = PlexServer(baseurl, token)
|
|
||||||
elif self.username and self.password and self.servername:
|
elif self.username and self.password and self.servername:
|
||||||
# Login via plex account
|
# Login via plex account
|
||||||
account = MyPlexAccount(self.username, self.password)
|
account = MyPlexAccount(self.username, self.password)
|
||||||
@@ -333,7 +361,6 @@ class Plex:
|
|||||||
user_plex = self.login(
|
user_plex = self.login(
|
||||||
self.plex._baseurl,
|
self.plex._baseurl,
|
||||||
user.get_token(self.plex.machineIdentifier),
|
user.get_token(self.plex.machineIdentifier),
|
||||||
self.ssl_bypass,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
libraries = user_plex.library.sections()
|
libraries = user_plex.library.sections()
|
||||||
@@ -397,8 +424,17 @@ class Plex:
|
|||||||
if self.admin_user == user:
|
if self.admin_user == user:
|
||||||
user_plex = self.plex
|
user_plex = self.plex
|
||||||
else:
|
else:
|
||||||
|
if isinstance(user, str):
|
||||||
|
logger(
|
||||||
|
f"Plex: {user} is not a plex object, attempting to get object for user",
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
user = self.plex.myPlexAccount().user(user)
|
||||||
|
|
||||||
user_plex = PlexServer(
|
user_plex = PlexServer(
|
||||||
self.plex._baseurl, user.get_token(self.plex.machineIdentifier)
|
self.plex._baseurl,
|
||||||
|
user.get_token(self.plex.machineIdentifier),
|
||||||
|
session=self.session,
|
||||||
)
|
)
|
||||||
|
|
||||||
for library, videos in libraries.items():
|
for library, videos in libraries.items():
|
||||||
|
|||||||
Reference in New Issue
Block a user