From b1ed46c64e689e0f22f1e94018a17f5c129d82a3 Mon Sep 17 00:00:00 2001 From: "B. Stack" Date: Wed, 31 Jan 2024 08:43:29 -0500 Subject: initial commit --- .gitignore | 1 + autocomplete-rescan.bash | 74 +++++++++++++++++++++++++++++ example-config-jellystack | 4 ++ jellystack.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++ jellystack_lib.py | 71 ++++++++++++++++++++++++++++ rescan-library.sh | 28 +++++++++++ 6 files changed, 294 insertions(+) create mode 100644 .gitignore create mode 100644 autocomplete-rescan.bash create mode 100644 example-config-jellystack create mode 100755 jellystack.py create mode 100644 jellystack_lib.py create mode 100755 rescan-library.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/autocomplete-rescan.bash b/autocomplete-rescan.bash new file mode 100644 index 0000000..28787b2 --- /dev/null +++ b/autocomplete-rescan.bash @@ -0,0 +1,74 @@ +#!/bin/bash +# File: autocomplete-rescan.bash +# Location: /mnt/public/Support/Programs/jellyfin/scripts/ +# Author: bgstack15, pawamoy +# Startdate: 2024-01-28-1 21:40 +# SPDX-License-Identifier: CC-BY-SA-4.0 +# Title: Bash autocompletion for rescan-library +# Project: jellystack +# Purpose: Make it easy to choose a jellyfin library to rescan +# History: +# Usage: +# dot-source it and then alias the script: +# . /mnt/public/Support/Programs/jellyfin/autocmplete-rescan.bash +# alias rescan-library=/mnt/public/Support/Programs/jellyfin/rescan-library.sh +# Reference: +# 1. https://stackoverflow.com/questions/44453510/how-to-autocomplete-a-bash-commandline-with-file-paths-from-a-specific-directory/47799826#47799826 +# 2. https://stackoverflow.com/questions/1146098/properly-handling-spaces-and-quotes-in-bash-completion +# Improve: +# Dependencies: +# password stored in ~/.jellyfin.password +# jellystack.py frontend and jellystack_lib.py + +_complete_specific_path() { + # ripped from https://stackoverflow.com/questions/44453510/how-to-autocomplete-a-bash-commandline-with-file-paths-from-a-specific-directory/47799826#47799826 and modified for jellystack + # alt which did not work: https://stackoverflow.com/questions/1146098/properly-handling-spaces-and-quotes-in-bash-completion + /mnt/public/Support/Programs/jellyfin/scripts/jellystack.py --autocomplete 1>/dev/null # populates ~/.cache/jellystack + # declare variables + local _item _COMPREPLY _old_pwd + thisdir=~/.cache/jellystack + + # if we already are in the completed directory, skip this part + _old_pwd="${PWD}" + # magic here: go the specific directory! + pushd "${thisdir}" &>/dev/null || return + + # init completion and run _filedir inside specific directory + _init_completion -s || return + _filedir + + # iterate on original replies + for _item in "${COMPREPLY[@]}"; do + # this check seems complicated, but it handles the case + # where you have files/dirs of the same name + # in the current directory and in the completed one: + # we want only one "/" appended + #if [ -d "${_item}" ] && [[ "${_item}" != */ ]] && [ ! -d "${_old_pwd}/${_item}" ]; then + # # append a slash if directory + # _COMPREPLY+=("${_item}/") + #else + _COMPREPLY+=("${_item}") + #fi + done + + # popd as early as possible + popd &>/dev/null + + # if only one reply and it is a directory, don't append a space + # (don't know why we must check for length == 2 though) + if [ ${#_COMPREPLY[@]} -eq 2 ]; then + if [[ "${_COMPREPLY}" == */ ]]; then + compopt -o nospace + fi + fi + + # set the values in the right COMPREPLY variable + COMPREPLY=( "${_COMPREPLY[@]}" ) + + # clean up + unset _COMPREPLY + unset _item +} + +complete -F _complete_specific_path rescan-library +alias rescan-library="$( dirname "$( readlink -f "${0}" )" )/rescan-library.sh" diff --git a/example-config-jellystack b/example-config-jellystack new file mode 100644 index 0000000..0e03d98 --- /dev/null +++ b/example-config-jellystack @@ -0,0 +1,4 @@ +# file: ~/.config/jellystack +# Also put password in ~/.jellyfin.password for the autocomplete to work +# Omit the trailing slash on the server name. +export password="eXamplePw$" username="admin" server="http://example.com:8096" diff --git a/jellystack.py b/jellystack.py new file mode 100755 index 0000000..bb9e2ac --- /dev/null +++ b/jellystack.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# File: jellystack.py +# Location: /mnt/public/Support/Programs/jellyfin/scripts/ +# Author: bgstack15 +# Startdate: 2024-01-29-2 09:25 +# SPDX-License-Identifier: GPL-3.0-only +# Title: jellyfin stackrpms cli frontend +# Project: jellystack +# Purpose: List and rescan individual libraries in jellyfin easier than navigating web ui +# History: +# Usage: +# from rescan-library.sh +# Reference: +# https://stackoverflow.com/questions/8258145/in-python-check-if-file-modification-time-is-older-than-a-specific-datetime +# Improve: +# Dependencies: +# jellystack_lib.py, python>=3.4 +# Documentation: +import jellystack_lib as js, os, argparse, shutil, sys +from datetime import datetime, timedelta +from pathlib import Path # python>=3.4 +js_debug = False + +# Ref: https://stackoverflow.com/questions/8258145/in-python-check-if-file-modification-time-is-older-than-a-specific-datetime +def is_file_older_than (file, delta): + cutoff = datetime.utcnow() - delta + mtime = datetime.utcfromtimestamp(os.path.getmtime(file)) + if mtime < cutoff: + return True + return False + +def check_cached_library_names(clear_cache = False): + """ + If cache is absent or older than 23 hours, generate new cache of library names + """ + cache_dir = os.path.join(os.path.expanduser("~"),".cache","jellystack") + _clear_cache = clear_cache + _cache_is_empty = True + try: + os.mkdir(cache_dir) + except: + pass + for thisdir, subdir, files in os.walk(cache_dir): + for tf in files: + if is_file_older_than(os.path.join(thisdir,tf), timedelta(hours=23)): + _clear_cache = True + if js_debug: + print(f"found too-old file {tf}, will clear the cache.",file=sys.stderr) + break + if _clear_cache: + shutil.rmtree(cache_dir) + if js_debug: + print(f"Clearing cache...",file=sys.stderr) + try: + os.mkdir(cache_dir) + except: + pass + # populate cache + try: + for thisdir, subdir, files in os.walk(cache_dir): + if len(files) > 0: + _cache_is_empty = False + except: + pass + if _cache_is_empty: + if js_debug: + print(f"Generating cache...",file=sys.stderr) + client = get_client() + lib_names = js.get_library_names_only(js.get_media_folders(client)) + #print(lib_names) + for name in lib_names: + Path(os.path.join(cache_dir,name)).touch() + # and now, list all the files in that path + for thisdir, subdir, files in os.walk(cache_dir): + for tf in files: + print(tf) + +def get_client(): + try: + client = js.get_authenticated_client(server,user,pw) + except: + raise Exception("Check password or url?") + return client + +if "__main__" == __name__: + server = os.environ.get("server","http://vm4:8096") + user = os.environ.get("username","admin") + pw = os.environ.get("password","none") + pwfile = os.path.join(os.path.expanduser("~"),".jellyfin.password") + if pw == "none": + #raise Exception(f"Set env var \"password\" and try again.") + if os.path.exists(pwfile): + try: + with open(pwfile,"r") as o: + pw = o.read().strip() + # the strip helps remove the newline that most people stick at the end of passwords in a passwordfile, and I seriously doubt the newline is part of the password. + except: + pass + parser = argparse.ArgumentParser() + parser.add_argument("--autocomplete",action="store_true",help="print library names, using the cache.") + parser.add_argument("--clear-cache",action="store_true",help="print library names, using the cache.") + parser.add_argument("--library",help="Rescan this library by name or uuid.") + args = parser.parse_args() + if type(args.library) == str: + args.library = args.library.rstrip("/") # to help with the bash autocomplete which always wants to add a trailing slash, if PWD has a directory named exactly the same as the library. I cannot find a way around it. + #print(args,file=sys.stderr) + if args.autocomplete: + check_cached_library_names(args.clear_cache) + else: + client = get_client() + folders = js.get_media_folders(client) + if args.library is not None: + js.refresh_library(args.library, folders, client) + else: + print("Use --autocomplete, or one of these for --library:") + js.libraries_to_csv(folders) diff --git a/jellystack_lib.py b/jellystack_lib.py new file mode 100644 index 0000000..194cfe2 --- /dev/null +++ b/jellystack_lib.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# File: jellystack_lib.py +# Location: /mnt/public/Support/Programs/jellyfin/scripts/ +# Author: bgstack15 +# Startdate: 2024-01-20-7 13:59 +# SPDX-License-Identifier: GPL-3.0-only +# Title: jellyfin stackrpms library +# Project: jellystack +# Purpose: Useful functions that use python3 jellyfin apiclient +# History: +# 2024-01 started for listing/rescanning libraries +# Usage: +# in jellystack.py +# client.auth.login("vm4:8096","admin","0EXAMPLE0f93EXAMPLEXAMPLE534e0f6") +# Reference: +# from firefox devtools: +# curl 'https://albion320.no-ip.biz:500/Items/14895ee3d991844aee62d94dd46dfb7a/Refresh?Recursive=true&ImageRefreshMode=Default&MetadataRefreshMode=Default&ReplaceAllImages=false&ReplaceAllMetadata=false' -X POST -H 'X-Emby-Authorization: MediaBrowser Client="Jellyfin Web", Device="Firefox", DeviceId="TW96aWxsYS81LjAgKFgxMTsgTGlEXAMPLEg2XzY0OyBydjo5MS4wKSBHZWNEXAMPLEEwMDEwMSBGaXJlZm94LzkxLjB8MTYzODk3NjExMzkwMA11", Version="10.8.10", Token="0f6f671c4eEXAMPLEc219aEXAMPLE7b3" -H 'Content-Length: 0' +# Improve: +# Dependencies: +# req-devuan: jellyfin-apiclient-python + +from jellyfin_apiclient_python import JellyfinClient +import json, os + +def get_authenticated_client(url = "http://vm4:8096", username = "admin", password = "None"): + client = JellyfinClient() + # this one works basically, but somehow my previous attempt did not. + client.config.app('cli','0.0.1','any-ssh-node','a56decad32c0aefd696a7d3565ac1d0') + client.config.data["auth.ssl"] = False + client.auth.connect_to_address(url) + client.auth.login(url,username,password) + credentials = client.auth.credentials.get_credentials() + server = credentials["Servers"][0] + server["username"] = 'username' + json.dumps(server) + #json.loads(credentials) + #client.authenticate({"Servers": [credentials]}, discover=False) + return client + +def get_media_folders(client): + results = client.jellyfin.get_media_folders()["Items"] + libraries = [{"Name":i["Name"],"Id":i["Id"]} for i in results] + return libraries + +def libraries_to_csv(libraries): + for i in libraries: + print(f"\"{i['Name']}\" {i['Id']}") + +def get_library_names_only(libraries): + return [i["Name"] for i in libraries] + +def refresh_library(library, libraries, client): + # find id of given library + _use_id = "" + for i in libraries: + if i["Id"] == library: + _use_id = library + break + elif i["Name"] == library: + _use_id = i["Id"] + break + # if not found, fail out + if _use_id == "": + raise f"Unable to find which library, given {library}" + print(f"Will try to refresh library {_use_id}") + client.http.request( + { + "type": "POST", + "url": f"{client.config.data['auth.server']}/Items/{_use_id}/Refresh?Recursive=true&ImageRefreshMode=Default&MetadataRefreshMode=Default&ReplaceAllImages=false&ReplaceAllMetadata=false" + } + ) diff --git a/rescan-library.sh b/rescan-library.sh new file mode 100755 index 0000000..230c78b --- /dev/null +++ b/rescan-library.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# File: rescan-library.sh +# Location: /mnt/public/Support/Programs/jellyfin/scripts/ +# Author: bgstack15 +# Startdate: 2024-01-20-7 13:09 +# SPDX-License-Identifier: GPL-3.0-only +# Title: rescan-library utility for jellyfin +# Project: jellystack +# Purpose: oneliner to rescan a specific library so I don't have to navigate web ui +# History: +# Usage: +# . /mnt/public/Support/Programs/jellyfin/scripts/autocmplete-rescan.bash +# alias rescan-library=/mnt/public/Support/Programs/jellyfin/scripts/rescan-library.sh +# rescan-library for autocomplete +# Reference: +# Improve: +# Dependencies: +# jellystack.py, jellystack_lib.py, python3 +# Documentation: +. ~/.config/jellystack +test -z "${password}" && export password="example" +test -z "${username}" && export username="admin" +test -z "${server}" && export server="http://example.com:8096" # omit the slash +executable="$( dirname "$( readlink -f "${0}" )" )/jellystack.py" +for word in "${@}" ; +do + python3 "${executable}" --library "${word}" +done -- cgit