Compare commits
19 Commits
Author | SHA1 | Date |
---|---|---|
Simon | f3661cba45 | |
crocs | 7f4feed2f2 | |
Florian Wetzel | e3e9c5fc68 | |
Simon | 3e0e13e18e | |
Simon | 6d7af2698e | |
Simon | 6978ca450d | |
Simon | 965d21cbb2 | |
Jayden | d71d26a4f1 | |
rockerbacon | e365f73018 | |
Simon | eb4558569c | |
Simon | 3c88c19eb1 | |
Simon | 248cc41839 | |
Simon | fade3babda | |
Simon | 73b8920a9c | |
Simon | 7a3c27a825 | |
Froghut | 51e7613880 | |
Froghut | 8026ba349d | |
Froghut | 25ae9bde60 | |
Simon | d7e690b822 |
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11.3-slim-bullseye
|
||||
FROM python:3.11.8-slim-bookworm
|
||||
ARG INSTALL_DEBUG
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PATH=/root/.local/bin:$PATH
|
||||
|
@ -7,12 +7,12 @@ ENV PATH=/root/.local/bin:$PATH
|
|||
RUN if [ "$INSTALL_DEBUG" ] ; then \
|
||||
apt-get -y update && apt-get -y install --no-install-recommends \
|
||||
vim htop bmon net-tools iputils-ping procps curl \
|
||||
&& pip install --user ipython \
|
||||
&& pip install ipython \
|
||||
; fi
|
||||
|
||||
# install requirements
|
||||
COPY ./requirements.txt /requirements.txt
|
||||
RUN pip install --user -r requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY app /app
|
||||
WORKDIR app
|
||||
|
|
60
README.md
60
README.md
|
@ -1,6 +1,18 @@
|
|||
## Tube Archivist Jellyfin Integration
|
||||
*Note: This repo was renamed from **tubearchivist/jellyfin** to **tubearchivist/tubearchivist-jf** to avoid confustion with the main Jellyfin repo.*
|
||||
## 📢 Important Announcement: Repository Deprecation and Migration
|
||||
We want to inform you that we are deprecating this repository,
|
||||
[tubearchivist-jf ](https://github.com/tubearchivist/tubearchivist-jf), and moving to an updated repo, [tubearchivist-jf-plugin](https://github.com/tubearchivist/tubearchivist-jf-plugin).
|
||||
This decision comes as part of our efforts to streamline our development process and provide a better experience for our users.
|
||||
|
||||
The new plugin offers better features, as well as a more futureproof codebase.
|
||||
|
||||
***Key Dates:***
|
||||
- [tubearchivist-jf ](https://github.com/tubearchivist/tubearchivist-jf) will be set to read-only on **July 20th, 2024**.
|
||||
|
||||
***What Does This Mean for You?***
|
||||
- We strongly suggest *not* installing this repo as it will stop receiving updates.
|
||||
- Transition to the new repo at your earliest convenience.
|
||||
|
||||
## Tube Archivist Jellyfin Integration
|
||||
Import your Tube Archivist media folder into Jellyfin
|
||||
|
||||
![home screenshot](assets/screenshot-home.png?raw=true "Jellyfin Home")
|
||||
|
@ -55,6 +67,7 @@ An example configuration is provided in the docker-compose.yml file. Configure t
|
|||
- `TA_TOKEN`: Tube Archivist API token, accessible from the settings page
|
||||
- `JF_URL`: Full URL where Jellyfin is reachable
|
||||
- `JF_TOKEN`: Jellyfin API token
|
||||
- `JF_FOLDER`: Folder override if your folder is not named "YouTube" on your Filesystem.
|
||||
- `LISTEN_PORT`: Optionally change the port where the integration is listening for messages. Defaults to `8001`. If you change this, make sure to also change the json link for auto trigger as described below.
|
||||
|
||||
Mount the `/youtube` folder from Tube Archivist also in this container at `/youtube` to give this integration access to the media archive.
|
||||
|
@ -62,11 +75,11 @@ Mount the `/youtube` folder from Tube Archivist also in this container at `/yout
|
|||
### Manual trigger
|
||||
For an initial import or for other irregular occasions, trigger the library scan from outside the container, e.g.:
|
||||
```bash
|
||||
docker exec -it tubearchivist-jf python main.py
|
||||
docker exec tubearchivist-jf python main.py
|
||||
```
|
||||
|
||||
### Auto trigger
|
||||
Use the notification functionality of Tube Archivist to automatically trigger a library scan whenever the download task complets in Tube Archivist. For the `Start download` schedule on your settings page add a json Apprise link to send a push notification to the `tubearchivist-jf` container on task completion, make sure to specify the port, e.g.:
|
||||
Use the notification functionality of Tube Archivist to automatically trigger a library scan whenever the download task completes in Tube Archivist. For the `Start download` schedule on your settings page add a json Apprise link to send a push notification to the `tubearchivist-jf` container on task completion, make sure to specify the port, e.g.:
|
||||
|
||||
```
|
||||
json://tubearchivist-jf:8001
|
||||
|
@ -85,42 +98,19 @@ pip install requests
|
|||
- `ta_token`: Tube Archivist API token, accessible from the settings page
|
||||
- `jf_url`: Full URL where Jellyfin is reachable
|
||||
- `jf_token`: Jellyfin API token
|
||||
- `jf_folder`: Name of the folder where TubeArchivist puts the files into
|
||||
|
||||
Then run the script from the main folder with python, e.g.
|
||||
```python
|
||||
python app/main.py
|
||||
```
|
||||
|
||||
## Migration problems
|
||||
Due to the filesystem change between Tube Archivist v0.3.6 to v0.4.0, this will reset your YouTube videos in Jellyfin and will add them as new again. Unfortunately there is no migration path.
|
||||
## Limitations
|
||||
You can only have *one* folder called **YouTube** in your Jellyfin.
|
||||
|
||||
To import an existing Tube Archivist archive created with v0.3.4 or before, there are a few manual steps needed. These issues are fixed with videos and channels indexed with v0.3.5 and later.
|
||||
Jellyfin needs to be able to see the temporary season folders created by this extensions. You will see messages like `waiting for seasons to be created` before you will run into a `TimeoutError`, if that doesn't happen in a reasonable time frame.
|
||||
|
||||
Apply these fixes *before* importing the archive.
|
||||
|
||||
**Permissions**
|
||||
Fix folder permissions not owned by the correct user. Navigate to the `ta_video_path` and run:
|
||||
|
||||
```bash
|
||||
sudo chown -R $UID:$GID .
|
||||
```
|
||||
|
||||
|
||||
**Channel Art**
|
||||
Tube Archivist v0.3.5 adds additional art work to the channel metadata. To trigger an automatic refresh of your old channels open a Python shell within the *tubearchivist* container:
|
||||
|
||||
```bash
|
||||
docker exec -it tubearchivist python
|
||||
```
|
||||
|
||||
Then execute these lines to trigger a background task for a full metadata refresh for all channels.
|
||||
|
||||
```python
|
||||
from home.src.es.connect import IndexPaginate
|
||||
from home.tasks import check_reindex
|
||||
|
||||
query = {"query": {"match_all": {}}}
|
||||
all_channels = IndexPaginate("ta_channel", query).get_results()
|
||||
reindex = {"channel": [i["channel_id"] for i in all_channels]}
|
||||
check_reindex.delay(data=reindex)
|
||||
```
|
||||
Some ideas for why that is:
|
||||
- Your JF busy, too slow or is already refreshing another library and is not picking up the folder in time.
|
||||
- JF doesn't have the permissions to see the folder created by the extension.
|
||||
- You didn't mount the volumes as expected and JF is looking in the wrong place.
|
||||
|
|
|
@ -38,6 +38,7 @@ def get_config_env() -> ConfigType | Literal[False]:
|
|||
"ta_token": os.environ["TA_TOKEN"],
|
||||
"jf_url": os.environ["JF_URL"],
|
||||
"jf_token": os.environ["JF_TOKEN"],
|
||||
"jf_folder": os.environ.get("JF_FOLDER", "youtube"),
|
||||
}
|
||||
return config_content
|
||||
|
||||
|
|
|
@ -9,7 +9,14 @@ from src.static_types import ConfigType, TAChannel, TAVideo
|
|||
|
||||
CONFIG: ConfigType = get_config()
|
||||
TIMEOUT = 60
|
||||
EXPECTED_ENV = {"ta_url", "ta_token", "jf_url", "jf_token", "ta_video_path"}
|
||||
EXPECTED_ENV = {
|
||||
"ta_url",
|
||||
"ta_token",
|
||||
"jf_url",
|
||||
"jf_token",
|
||||
"ta_video_path",
|
||||
"jf_folder",
|
||||
}
|
||||
|
||||
|
||||
class Jellyfin:
|
||||
|
|
|
@ -22,8 +22,10 @@ class Library:
|
|||
"""get collection id for youtube folder"""
|
||||
path: str = "Items?Recursive=true&includeItemTypes=Folder"
|
||||
folders: dict = Jellyfin().get(path)
|
||||
folder_name: str = get_config()["jf_folder"]
|
||||
|
||||
for folder in folders["Items"]:
|
||||
if folder.get("Name").lower() == "youtube":
|
||||
if folder.get("Name").lower() == folder_name.lower():
|
||||
return folder.get("Id")
|
||||
|
||||
raise ValueError("youtube folder not found")
|
||||
|
@ -45,7 +47,9 @@ class Library:
|
|||
|
||||
def _get_all_series(self) -> dict:
|
||||
"""get all shows indexed in jf"""
|
||||
path: str = f"Items?Recursive=true&IncludeItemTypes=Series&fields=ParentId,Path&ParentId={self.yt_collection}" # noqa: E501
|
||||
path: str = (
|
||||
f"Items?Recursive=true&IncludeItemTypes=Series&fields=ParentId,Path&ParentId={self.yt_collection}" # noqa: E501
|
||||
)
|
||||
all_shows: dict = Jellyfin().get(path)
|
||||
|
||||
return all_shows
|
||||
|
@ -69,10 +73,12 @@ class Library:
|
|||
|
||||
def refresh_collection(self, collection_id: str) -> None:
|
||||
"""trigger collection refresh"""
|
||||
path: str = f"Items/{collection_id}/Refresh?Recursive=true&ImageRefreshMode=Default&MetadataRefreshMode=Default" # noqa: E501
|
||||
path: str = (
|
||||
f"Items/{collection_id}/Refresh?Recursive=true&ImageRefreshMode=Default&MetadataRefreshMode=Default" # noqa: E501
|
||||
)
|
||||
Jellyfin().post(path, False)
|
||||
|
||||
for _ in range(12):
|
||||
while True:
|
||||
response = Jellyfin().get("Library/VirtualFolders")
|
||||
for folder in response:
|
||||
if not folder["ItemId"] == collection_id:
|
||||
|
@ -82,7 +88,7 @@ class Library:
|
|||
return
|
||||
|
||||
print("waiting for library refresh")
|
||||
sleep(5)
|
||||
sleep(10)
|
||||
|
||||
|
||||
class Show:
|
||||
|
@ -101,7 +107,7 @@ class Show:
|
|||
|
||||
def _get_ta_channel(self) -> TAChannel | None:
|
||||
"""get ta channel metadata"""
|
||||
channel_id: str = self.show["Path"].split("/")[-1]
|
||||
channel_id: str = self.show["Path"].replace("\\", "/").split("/")[-1]
|
||||
ta_channel: TAChannel | None = TubeArchivist().get_channel(channel_id)
|
||||
|
||||
return ta_channel
|
||||
|
@ -155,7 +161,9 @@ class Show:
|
|||
print(f"[show][{showname}] indexing {len(new_episodes)} videos")
|
||||
seasons_created: list[str] = []
|
||||
for jf_ep in new_episodes:
|
||||
youtube_id: str = os.path.basename(jf_ep["Path"]).split(".")[0]
|
||||
youtube_id: str = os.path.basename(
|
||||
jf_ep["Path"].replace("\\", "/")
|
||||
).split(".")[0]
|
||||
episode_handler = Episode(youtube_id, jf_ep["Id"])
|
||||
ta_video: TAVideo = episode_handler.get_ta_video()
|
||||
season_folder: str | None = self.create_season(ta_video, jf_ep)
|
||||
|
@ -186,9 +194,17 @@ class Show:
|
|||
return None
|
||||
|
||||
base: str = get_config()["ta_video_path"]
|
||||
channel_folder = os.path.split(os.path.split(jf_ep["Path"])[0])[-1]
|
||||
channel_folder = os.path.split(
|
||||
os.path.split(jf_ep["Path"].replace("\\", "/"))[0]
|
||||
)[-1]
|
||||
season_folder = os.path.join(base, channel_folder, expected_season)
|
||||
os.makedirs(season_folder)
|
||||
if not os.path.exists(season_folder):
|
||||
original_umask = os.umask(0)
|
||||
try:
|
||||
os.mkdir(season_folder, mode=0o777)
|
||||
finally:
|
||||
os.umask(original_umask)
|
||||
|
||||
self._wait_for_season(expected_season)
|
||||
|
||||
return season_folder
|
||||
|
@ -196,11 +212,16 @@ class Show:
|
|||
def _wait_for_season(self, expected_season: str) -> None:
|
||||
"""wait for season to be created in JF"""
|
||||
jf_id: str = self.show["Id"]
|
||||
path: str = f"Items/{jf_id}/Refresh?Recursive=true&ImageRefreshMode=Default&MetadataRefreshMode=Default" # noqa: E501
|
||||
path: str = (
|
||||
f"Items/{jf_id}/Refresh?Recursive=true&ImageRefreshMode=Default&MetadataRefreshMode=Default" # noqa: E501
|
||||
)
|
||||
print(f"[setup] {path=}")
|
||||
Jellyfin().post(path, False)
|
||||
for _ in range(12):
|
||||
for _ in range(24):
|
||||
all_existing: set[str] = set(self._get_existing_seasons())
|
||||
|
||||
print(f"[setup] seasons: {all_existing} {expected_season=}")
|
||||
|
||||
if expected_season in all_existing:
|
||||
return
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ class ConfigType(TypedDict):
|
|||
ta_token: str
|
||||
jf_url: str
|
||||
jf_token: str
|
||||
jf_folder: str
|
||||
|
||||
|
||||
class TAChannel(TypedDict):
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
"ta_url": "http://tubearchivist.local",
|
||||
"ta_token": "xxxxxxxxxxxxxxxx",
|
||||
"jf_url": "http://jellyfin.local:8096",
|
||||
"jf_token": "yyyyyyyyyyyyyyyy"
|
||||
"jf_token": "yyyyyyyyyyyyyyyy",
|
||||
"jf_folder": "YouTube"
|
||||
}
|
||||
|
|
|
@ -17,12 +17,12 @@ function validate {
|
|||
echo "running black"
|
||||
black --force-exclude "migrations/*" --diff --color --check -l 79 "$check_path"
|
||||
echo "running codespell"
|
||||
codespell --skip="./.git,./.venv,./.mypy_cache" "$check_path"
|
||||
codespell --skip="./.git,./.venv,venv,./.mypy_cache" "$check_path"
|
||||
echo "running flake8"
|
||||
flake8 "$check_path" --exclude "migrations,.venv" --count --max-complexity=10 \
|
||||
flake8 "$check_path" --exclude "migrations,.venv,venv" --count --max-complexity=10 \
|
||||
--max-line-length=79 --show-source --statistics
|
||||
echo "running isort"
|
||||
isort --skip "migrations" --skip ".venv" --check-only --diff --profile black -l 79 "$check_path"
|
||||
isort --skip "migrations" --skip ".venv" --skip "venv" --check-only --diff --profile black -l 79 "$check_path"
|
||||
printf " \n> all validations passed\n"
|
||||
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
Flask==2.3.3
|
||||
requests==2.31.0
|
||||
Flask==2.3.2
|
||||
|
|
Loading…
Reference in New Issue