Minor cleanup and tweaks

This commit is contained in:
Phin 2024-08-30 20:29:02 +05:30
parent 5461ad20ed
commit 51e54394cf
12 changed files with 45 additions and 32 deletions

View file

@ -3,7 +3,10 @@
.github .github
.gitignore .gitignore
CONTRIBUTING.md CONTRIBUTING.md
data
Dockerfile Dockerfile
lint.bat
pyrightconfig.json pyrightconfig.json
__pycache__
assets/showcase.psd assets/showcase.psd
data

View file

@ -18,7 +18,7 @@ jobs:
- name: Create release ZIP file - name: Create release ZIP file
run: |- run: |-
mv .github/release-notes ../ mv .github/release-notes ../
rm -rf .git .github .gitignore .dockerignore Dockerfile CONTRIBUTING.md pyrightconfig.json xargs rm -rf < .dockerignore
mkdir ${{ github.event.repository.name }} mkdir ${{ github.event.repository.name }}
mv * ${{ github.event.repository.name }} || true mv * ${{ github.event.repository.name }} || true
zip -r $RELEASE_ZIP_FILENAME ${{ github.event.repository.name }} zip -r $RELEASE_ZIP_FILENAME ${{ github.event.repository.name }}

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
data __pycache__
assets/showcase.psd assets/showcase.psd
data

View file

@ -15,6 +15,7 @@ logFilePath = os.path.join(dataDirectoryPath, "console.log")
isUnix = sys.platform in ["linux", "darwin"] isUnix = sys.platform in ["linux", "darwin"]
processID = os.getpid() processID = os.getpid()
isInteractive = sys.stdin and sys.stdin.isatty() isInteractive = sys.stdin and sys.stdin.isatty()
plexServerNameInput = os.environ.get("DRPP_PLEX_SERVER_NAME_INPUT")
noPipInstall = os.environ.get("DRPP_NO_PIP_INSTALL", "") == "true" noPipInstall = os.environ.get("DRPP_NO_PIP_INSTALL", "") == "true"
isInContainer = os.environ.get("DRPP_IS_IN_CONTAINER", "") == "true" isInContainer = os.environ.get("DRPP_IS_IN_CONTAINER", "") == "true"
runtimeDirectory = "/run/app" if isInContainer else os.environ.get("XDG_RUNTIME_DIR", os.environ.get("TMPDIR", os.environ.get("TMP", os.environ.get("TEMP", "/tmp")))) runtimeDirectory = "/run/app" if isInContainer else os.environ.get("XDG_RUNTIME_DIR", os.environ.get("TMPDIR", os.environ.get("TMP", os.environ.get("TEMP", "/tmp"))))

View file

@ -49,9 +49,9 @@ def loadConfig() -> None:
try: try:
with open(configFilePath, "r", encoding = "UTF-8") as configFile: with open(configFilePath, "r", encoding = "UTF-8") as configFile:
if configFileType == "yaml": if configFileType == "yaml":
loadedConfig = yaml.safe_load(configFile) or {} loadedConfig = yaml.safe_load(configFile) or {} # pyright: ignore[reportUnknownVariableType]
else: else:
loadedConfig = json.load(configFile) or {} loadedConfig = json.load(configFile) or {} # pyright: ignore[reportUnknownVariableType]
except: except:
os.rename(configFilePath, f"{configFilePathBase}-{time.time():.0f}.{configFileExtension}") os.rename(configFilePath, f"{configFilePathBase}-{time.time():.0f}.{configFileExtension}")
logger.exception("Failed to parse the config file. A new one will be created.") logger.exception("Failed to parse the config file. A new one will be created.")

View file

@ -6,12 +6,13 @@ import io
import models.imgur import models.imgur
import requests import requests
def uploadToImgur(url: str, maxSize: int = 0) -> Optional[str]: def uploadToImgur(url: str) -> Optional[str]:
try: try:
originalImageBytesIO = io.BytesIO(requests.get(url).content) originalImageBytesIO = io.BytesIO(requests.get(url).content)
originalImage = Image.open(originalImageBytesIO) originalImage = Image.open(originalImageBytesIO)
newImage = Image.new("RGB", originalImage.size) newImage = Image.new("RGB", originalImage.size)
newImage.putdata(originalImage.getdata()) # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType] newImage.putdata(originalImage.getdata()) # pyright: ignore[reportArgumentType]
maxSize = config["display"]["posters"]["maxSize"]
if maxSize: if maxSize:
newImage.thumbnail((maxSize, maxSize)) newImage.thumbnail((maxSize, maxSize))
newImageBytesIO = io.BytesIO() newImageBytesIO = io.BytesIO()

View file

@ -1,11 +1,10 @@
# pyright: reportUnknownArgumentType=none,reportUnknownMemberType=none,reportUnknownVariableType=none # pyright: reportUnknownArgumentType=none,reportUnknownMemberType=none,reportUnknownVariableType=none,reportTypedDictNotRequiredAccess=none,reportOptionalMemberAccess=none,reportMissingTypeStubs=none
from .config import config from .config import config
from .discord import DiscordIpcService from .discord import DiscordIpcService
from .imgur import uploadToImgur from .imgur import uploadToImgur
from config.constants import name, plexClientID from config.constants import name, plexClientID
from plexapi.alert import AlertListener from plexapi.alert import AlertListener
from plexapi.base import PlexSession, PlexPartialObject
from plexapi.media import Genre, Guid from plexapi.media import Genre, Guid
from plexapi.myplex import MyPlexAccount, PlexServer from plexapi.myplex import MyPlexAccount, PlexServer
from typing import Optional from typing import Optional
@ -56,7 +55,7 @@ class PlexAlertListener(threading.Thread):
self.daemon = True self.daemon = True
self.token = token self.token = token
self.serverConfig = serverConfig self.serverConfig = serverConfig
self.logger = LoggerWithPrefix(f"[{self.serverConfig['name']}] ") # pyright: ignore[reportTypedDictNotRequiredAccess] self.logger = LoggerWithPrefix(f"[{self.serverConfig['name']}] ")
self.discordIpcService = DiscordIpcService(self.serverConfig.get("ipcPipeNumber")) self.discordIpcService = DiscordIpcService(self.serverConfig.get("ipcPipeNumber"))
self.updateTimeoutTimer: Optional[threading.Timer] = None self.updateTimeoutTimer: Optional[threading.Timer] = None
self.connectionCheckTimer: Optional[threading.Timer] = None self.connectionCheckTimer: Optional[threading.Timer] = None
@ -94,7 +93,7 @@ class PlexAlertListener(threading.Thread):
if not self.server: if not self.server:
raise Exception("Server not found") raise Exception("Server not found")
except Exception as e: except Exception as e:
self.logger.error("Failed to connect to %s '%s': %s", self.productName, self.serverConfig["name"], e) # pyright: ignore[reportTypedDictNotRequiredAccess] self.logger.error("Failed to connect to %s '%s': %s", self.productName, self.serverConfig["name"], e)
self.logger.error("Reconnecting in 10 seconds") self.logger.error("Reconnecting in 10 seconds")
time.sleep(10) time.sleep(10)
@ -154,11 +153,11 @@ class PlexAlertListener(threading.Thread):
self.logger.debug("Received alert: %s", stateNotification) self.logger.debug("Received alert: %s", stateNotification)
ratingKey = int(stateNotification["ratingKey"]) ratingKey = int(stateNotification["ratingKey"])
assert self.server assert self.server
item: PlexPartialObject = self.server.fetchItem(ratingKey) item = self.server.fetchItem(ratingKey)
if item.key and item.key.startswith("/livetv"): if item.key and item.key.startswith("/livetv"):
mediaType = "live_episode" mediaType = "live_episode"
else: else:
mediaType: str = item.type mediaType = item.type
if mediaType not in validMediaTypes: if mediaType not in validMediaTypes:
self.logger.debug("Unsupported media type '%s', ignoring", mediaType) self.logger.debug("Unsupported media type '%s', ignoring", mediaType)
return return
@ -166,7 +165,7 @@ class PlexAlertListener(threading.Thread):
sessionKey = int(stateNotification["sessionKey"]) sessionKey = int(stateNotification["sessionKey"])
viewOffset = int(stateNotification["viewOffset"]) viewOffset = int(stateNotification["viewOffset"])
try: try:
libraryName: str = item.section().title libraryName = item.section().title
except: except:
libraryName = "ERROR" libraryName = "ERROR"
if "blacklistedLibraries" in self.serverConfig and libraryName in self.serverConfig["blacklistedLibraries"]: if "blacklistedLibraries" in self.serverConfig and libraryName in self.serverConfig["blacklistedLibraries"]:
@ -195,7 +194,7 @@ class PlexAlertListener(threading.Thread):
return return
if self.isServerOwner: if self.isServerOwner:
self.logger.debug("Searching sessions for session key %s", sessionKey) self.logger.debug("Searching sessions for session key %s", sessionKey)
sessions: list[PlexSession] = self.server.sessions() sessions = self.server.sessions()
if len(sessions) < 1: if len(sessions) < 1:
self.logger.debug("Empty session list, ignoring") self.logger.debug("Empty session list, ignoring")
return return
@ -203,7 +202,7 @@ class PlexAlertListener(threading.Thread):
self.logger.debug("%s, Session Key: %s, Usernames: %s", session, session.sessionKey, session.usernames) self.logger.debug("%s, Session Key: %s, Usernames: %s", session, session.sessionKey, session.usernames)
if session.sessionKey == sessionKey: if session.sessionKey == sessionKey:
self.logger.debug("Session found") self.logger.debug("Session found")
sessionUsername: str = session.usernames[0] sessionUsername = session.usernames[0]
if sessionUsername.lower() == self.listenForUser.lower(): if sessionUsername.lower() == self.listenForUser.lower():
self.logger.debug("Username '%s' matches '%s', continuing", sessionUsername, self.listenForUser) self.logger.debug("Username '%s' matches '%s', continuing", sessionUsername, self.listenForUser)
break break
@ -268,7 +267,7 @@ class PlexAlertListener(threading.Thread):
thumbUrl = getCacheKey(thumb) thumbUrl = getCacheKey(thumb)
if not thumbUrl or not isinstance(thumbUrl, str): if not thumbUrl or not isinstance(thumbUrl, str):
self.logger.debug("Uploading poster to Imgur") self.logger.debug("Uploading poster to Imgur")
thumbUrl = uploadToImgur(self.server.url(thumb, True), config["display"]["posters"]["maxSize"]) thumbUrl = uploadToImgur(self.server.url(thumb, True))
setCacheKey(thumb, thumbUrl) setCacheKey(thumb, thumbUrl)
activity: models.discord.Activity = { activity: models.discord.Activity = {
"details": truncate(title, 128), "details": truncate(title, 128),
@ -326,9 +325,9 @@ class PlexAlertListener(threading.Thread):
if state == "playing": if state == "playing":
currentTimestamp = int(time.time()) currentTimestamp = int(time.time())
if config["display"]["useRemainingTime"]: if config["display"]["useRemainingTime"]:
activity["timestamps"] = {"end": round(currentTimestamp + ((item.duration - viewOffset) / 1000))} activity["timestamps"] = { "end": round(currentTimestamp + ((item.duration - viewOffset) / 1000)) }
else: else:
activity["timestamps"] = {"start": round(currentTimestamp - (viewOffset / 1000))} activity["timestamps"] = { "start": round(currentTimestamp - (viewOffset / 1000)) }
if not self.discordIpcService.connected: if not self.discordIpcService.connected:
self.discordIpcService.connect() self.discordIpcService.connect()
if self.discordIpcService.connected: if self.discordIpcService.connected:

2
lint.bat Normal file
View file

@ -0,0 +1,2 @@
@echo off
pyright --pythonversion 3.10.0

18
main.py
View file

@ -13,7 +13,7 @@ if isInContainer:
uid, gid = statResult.st_uid, statResult.st_gid uid, gid = statResult.st_uid, statResult.st_gid
else: else:
if noRuntimeDirChown: if noRuntimeDirChown:
logger.warning(f"DRPP_NO_RUNTIME_DIR_CHOWN is set to true. Manually ensure appropriate ownership of {runtimeDirectory}") logger.warning(f"Environment variable DRPP_NO_RUNTIME_DIR_CHOWN is set to true. Manually ensure appropriate ownership of {runtimeDirectory}")
else: else:
os.system(f"chmod 700 {runtimeDirectory}") os.system(f"chmod 700 {runtimeDirectory}")
os.system(f"chown -R {uid}:{gid} {runtimeDirectory}") os.system(f"chown -R {uid}:{gid} {runtimeDirectory}")
@ -21,7 +21,7 @@ if isInContainer:
os.setgid(gid) # pyright: ignore[reportAttributeAccessIssue,reportUnknownMemberType] os.setgid(gid) # pyright: ignore[reportAttributeAccessIssue,reportUnknownMemberType]
os.setuid(uid) # pyright: ignore[reportAttributeAccessIssue,reportUnknownMemberType] os.setuid(uid) # pyright: ignore[reportAttributeAccessIssue,reportUnknownMemberType]
else: else:
logger.warning(f"Not running as the superuser. Manually ensure appropriate ownership of mounted contents") logger.warning("Not running as the superuser. Manually ensure appropriate ownership of mounted contents")
from config.constants import noPipInstall from config.constants import noPipInstall
import sys import sys
@ -43,7 +43,7 @@ if not noPipInstall:
except Exception as e: except Exception as e:
logger.exception("An unexpected error occured during automatic installation of dependencies. Install them manually by running the following command: python -m pip install -U -r requirements.txt") logger.exception("An unexpected error occured during automatic installation of dependencies. Install them manually by running the following command: python -m pip install -U -r requirements.txt")
from config.constants import dataDirectoryPath, logFilePath, name, version, isInteractive from config.constants import dataDirectoryPath, logFilePath, name, version, isInteractive, plexServerNameInput
from core.config import config, loadConfig, saveConfig from core.config import config, loadConfig, saveConfig
from core.discord import DiscordIpcService from core.discord import DiscordIpcService
from core.plex import PlexAlertListener, initiateAuth, getAuthToken from core.plex import PlexAlertListener, initiateAuth, getAuthToken
@ -72,7 +72,6 @@ def init() -> None:
loadCache() loadCache()
def main() -> None: def main() -> None:
init()
if not config["users"]: if not config["users"]:
logger.info("No users found in the config file") logger.info("No users found in the config file")
user = authNewUser() user = authNewUser()
@ -104,9 +103,15 @@ def authNewUser() -> Optional[models.config.User]:
authToken = getAuthToken(id, code) authToken = getAuthToken(id, code)
if authToken: if authToken:
logger.info("Authentication successful") logger.info("Authentication successful")
serverName = os.environ.get("DRPP_PLEX_SERVER_NAME_INPUT") serverName = plexServerNameInput
if not serverName: if not serverName:
serverName = input("Enter the name of the Plex Media Server you wish to connect to: ") if isInteractive else "ServerName" if isInteractive:
serverName = input("Enter the name of the Plex Media Server to connect to: ")
else:
serverName = "ServerName"
logger.warning("Environment variable DRPP_PLEX_SERVER_NAME_INPUT is not set and the environment is non-interactive")
logger.warning("\"ServerName\" will be used as a placeholder for the name of the Plex Media Server to connect to")
logger.warning("Change this by editing the config file and restarting the script")
return { "token": authToken, "servers": [{ "name": serverName }] } return { "token": authToken, "servers": [{ "name": serverName }] }
time.sleep(5) time.sleep(5)
else: else:
@ -134,6 +139,7 @@ if __name__ == "__main__":
mode = sys.argv[1] if len(sys.argv) > 1 else "" mode = sys.argv[1] if len(sys.argv) > 1 else ""
try: try:
if not mode: if not mode:
init()
main() main()
elif mode == "test-ipc": elif mode == "test-ipc":
testIpc(int(sys.argv[2]) if len(sys.argv) > 2 else -1) testIpc(int(sys.argv[2]) if len(sys.argv) > 2 else -1)

View file

@ -1,3 +1,3 @@
{ {
"reportMissingTypeStubs": "information" "strict": ["**"]
} }

View file

@ -1,5 +1,5 @@
PlexAPI==4.15.9 PlexAPI==4.15.16
requests==2.31.0 requests==2.32.3
websocket-client==1.7.0 websocket-client==1.8.0
PyYAML==6.0.1 PyYAML==6.0.2
Pillow==10.2.0 Pillow==10.4.0

View file

@ -2,7 +2,7 @@ from typing import Optional
def formatSeconds(seconds: int | float, joiner: Optional[str] = None) -> str: def formatSeconds(seconds: int | float, joiner: Optional[str] = None) -> str:
seconds = round(seconds) seconds = round(seconds)
timeValues = {"h": seconds // 3600, "m": seconds // 60 % 60, "s": seconds % 60} timeValues = { "h": seconds // 3600, "m": seconds // 60 % 60, "s": seconds % 60 }
if not joiner: if not joiner:
return "".join(str(v) + k for k, v in timeValues.items() if v > 0) return "".join(str(v) + k for k, v in timeValues.items() if v > 0)
if timeValues["h"] == 0: if timeValues["h"] == 0: