Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Looking for Clarification regarding use in Ubuntu #22

Open
Deathproof76 opened this issue Sep 1, 2023 · 18 comments
Open

Looking for Clarification regarding use in Ubuntu #22

Deathproof76 opened this issue Sep 1, 2023 · 18 comments

Comments

@Deathproof76
Copy link

Deathproof76 commented Sep 1, 2023

Hello and thank you for your work on PlexCache and also the wiki! I would love to use it, but I still have some questions regarding its setup outside of unraid.

I'm using Ubuntu without bcache or similar things , just straight up ext4 formatted drives and plex in docker and for example:

I have an empty ext4 formatted ssd and a hdd where the media folder for my plex docker is mounted to. I tell the symlink version of the script that the cache folder is on the sdd. What happens if I start the script? Will the specified movies/shows be moved to the cachefolder on the ssd and in their place on the hdd there'll only be a symlink left? So that means I would have to mount the cache folder in the container too. Won't the hdd be woken up anyways when the symlink is accessed by plex?

Just an Idea, which I will look up later: Can't the files simply be copied to the ssd whilst also remaining on the hdd and renamed in such a way that plex prefers the file on the ssd?

@bexem
Copy link
Owner

bexem commented Sep 1, 2023

Hello and thank you for your work on PlexCache and also the wiki! I would love to use it, but I still have some questions regarding its setup outside of unraid.

Hi there to you too!

I'm using Ubuntu without bcache or similar things , just straight up ext4 formatted drives and plex in docker and for example:

I have an empty ext4 formatted ssd and a hdd where the media folder for my plex docker is mounted to. I tell the symlink version of the script that the cache folder is on the sdd. What happens if I start the script? Will the specified movies/shows be moved to the cachefolder on the ssd and in their place on the hdd there'll only be a symlink left?

Yes, exactly, you got it right.

So that means I would have to mount the cache folder in the container too. Won't the hdd be woken up anyways when the symlink is accessed by plex?

You would need to do that (map the cache drive to the container) and yes that would happen but then it would spin down as it would not be actively used anymore because plex would then read the file from the cache drive (in theory).
I know it's not ideal, that's why it has been stale that code for a long while, and still untested. 😕
Maybe using a sort of a FUSE system like unraid might be beneficial, but that would not be a job for the script as it will need to be configured depending on the OS (in your case ubuntu). Or maybe there is a better way I am not aware (yet).

Just an Idea, which I will look up later: Can't the files simply be copied to the ssd whilst also remaining on the hdd and renamed in such a way that plex prefers the file on the ssd?

I would not know how plex would "prefer" the file in the ssd, would it not need like two libraries? Like the script would keep that FastMovies and FastTVSeries libraries up-to-date keeping in the just the ondeck/watchlist files? But it would get confusing on the clients as they would have duplicated media on the homepage (An OnDeck version of the episode of the slow library and another for the fast one)

I am fully open to suggestions as I like to improve the code and help and include other users, but unraid is the only system I actively support as is the one I use.

@Deathproof76
Copy link
Author

Deathproof76 commented Sep 1, 2023

Plex has this multiple editions/versions/etc. feature , which should be able to make this idea work without extra library confusion:

Screenshot 2023-09-01 203408

One library, three folders to search for content. I copypasted the same moviefolder to all three, didn't change or rename anything. Plex understands that it's the same movie and merges it.

Screenshot 2023-09-01 202827+

I also tried the same thing with a show (without tmdbid) and it works too. For whole shows, seasons, episodes:

Screenshot 2023-09-01 204157
It doesn't matter if it's across storage devices, as long as it's recognized as the same movie/show by the scanner.

The trickier part was finding out how plex decides what to play first. I'll assume that the uppermost version is the one that`ll be played first, when pressing the play button, without choosing a version (still have to actually confirm via iotop for example, that my assumption is watertight, will do when I've the time). btw: "Play Version" a new button that pops up after multipling the files this way.

After playing around a bit (renaming files, adding libraryfolders/files in different sequences, copying movies to empty libraryfolder, etc.). I found out that the order isn't alphabetically and so far it seems most likely that plex chooses to play the version of the file, that was first discovered by it. So for example folder a is empty, folder b is empty -> put movie in folder b, copy movie to folder a => version in folder b will always be played first, no matter the alphabetical order.

There are still some things I could try this way, but none seem useful in the context of: copy the, as from plex as identical recognized, movie to cache and play from there first.

But yeah, it should work in theory, which would make it possible to let the hdd sleep and also avoid the move to cache and then move back to hdd and rescan/redo preview thumbnails stuff. There ought to be a possibility to rename the files for playorder (when directly pressing play without choosing the version), maybe even a "set priority-libraryfolder"-option from within plex or via plexapi, maybe a modified media scanner but I haven't seen anything at the first few glances. I'll look further when I have the time.

@bexem
Copy link
Owner

bexem commented Sep 1, 2023

That's great!
So I cannot test it because currently I don't even have a cache drive because mine is dying and I'm away, but I've written a tiny script which given your plex details and the cache path, will scan your main user onDeck media, for each media will check all the available versions, then, for each media, when found the one contained in the given cache path, will set that as preferred version.
https://pastebin.com/cfWxCnec

I have no idea if it works properly, let me know, if it does, we can work on it!

Forgot to say: you do need to manually create a version of the media (I don't know if just a duplicate is enough or you also need to rename it slightly). The script right now does no file manipulation.

@Deathproof76
Copy link
Author

Deathproof76 commented Sep 1, 2023

Wow thanks!

Okay, this is what I got so far:

max@Server-Zero:/mnt/cache/test2$ python preferplex.py
Processing: 1:23:45
Checking File Path: /mnt/i_terrorbyte/Deutsch/Serien/Chernobyl/Season 1/Chernobyl - S01E01 - 1-23-45 (tt7366338).mkv
Processing: 1:23:45
Checking File Path: /mnt/the_pool/test2/Chernobyl/Season 1/Chernobyl - S01E01 - 1-23-45 (tt7366338).mkv
Checking File Path: /mnt/cache/test2/Chernobyl/Season 1/Chernobyl - S01E01 - 1-23-45 (tt7366338).mkv
Setting the preferred version to: 545087 with path: /mnt/cache/test2/Chernobyl/Season 1/Chernobyl - S01E01 - 1-23-45 (tt7366338).mkv
Traceback (most recent call last):
  File "/mnt/cache/test2/preferplex.py", line 40, in <module>
    item.preferredVersion(preferred_version.id)
    ^^^^^^^^^^^^^^^^^^^^^
  File "/home/max/.local/lib/python3.11/site-packages/plexapi/base.py", line 516, in __getattribute__
    value = super(PlexPartialObject, self).__getattribute__(attr)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Episode' object has no attribute 'preferredVersion'

just fyi: I have a separate mirrored library in a different language which doesn't have the cachepath as a folder. Seems like that contributes to the error?

@bexem
Copy link
Owner

bexem commented Sep 1, 2023

Thank you for trying, oddly it was giving an error but apparently the versioning of the media is actually not (hopefully yet), I'm now having a look at the whole plexapi documentation, I honestly thought it was there but I was clearly very wrong.
I'll keep you posted.
In the meanwhile if you have another idea to approach the problem, let me know!

@Deathproof76
Copy link
Author

Deathproof76 commented Sep 1, 2023

I'm sure that there will be a way, I will definitely check out plexapi myself tomorrow (already past midnight here). Plexforums/tagging/plexmatch or similiar from vanillaplex led to nothing.
Could possibly work via mergerfs using tieredcaching: 1. Pool the drives including cache together 2. give plex the pooled path 3. but use script to copy from source to the actual mounted cachedrive 4. yeah, and then some magic to get mergerfs to give preferred read directions to the basically duplicate files on cache drive, idk have to read more.... 😅

But so far I could confirm via iotop that the uppermost file is played by default. So there's definitely at least this hacky manual plexdance I found out that could be scripted:

  1. both source and cachefolder added in library
  2. moviefolder from source is copied to cache
  3. run scan in plex
  4. rename source "moviefolder" to "moviefolder-"
  5. run scan again
  6. rename source "moviefolder-" back to "moviefolder" (I don't know why, but from what I can tell it won't work otherwise)
  7. scan library again and the cachepath will be the first and default play path.

Thanks again, I'll keep you posted too!

@bexem
Copy link
Owner

bexem commented Sep 1, 2023

Uhm...So the script would need to:

  1. Fetch the media respectively on onDeck and Watchlist;
  2. Copy the media to the cache path;
  3. Run scan; library.update()
  4. Rename the source media folder (Is the "-" random or is it part of the workaround?) *
  5. Run scan; library.update()
  6. Rename the source back to the original name.
  7. Run scan; library.update()
  8. Profit.

*And what about the tv series/anime? It would break all the other episodes if we rename the whole Show folder and/or the Season folder. I understand is temporary, so it might actually be fine.

I had a look at the plexapi documentation again and I see no trace of setting the favourite version or edition, but I hope I'm just blind 😅

@Deathproof76
Copy link
Author

Deathproof76 commented Sep 1, 2023

the "-" is random. Will answer in more detail tomorrow, seriously tired.

I found that after doing this "plexdance" the Media ids of the duplicate files are changin (look at episode -> media info> view xml). first the default played, uppermost file ( the one on the hdd) had Media id="545138" and the file on the cache <Media id="545226". After the dance the cachefile had same id but the file on hdd was <Media id="545250". Notice the number increase. Pretty sure that's what actually corresponds to the default play order (If everything else is the same). Maybe it's possible to "reindex" those specific files and force the change of defaultplay that way?

  1. Fetch the media respectively on onDeck and Watchlist;
  2. Copy the media to the cache path;
  3. Run scan; library.update()
  4. Force reindexing of the fetched media thats not on cachepath and don't break anything, hopefully, because the copied files/versions supplant the original ones?

*And what about the tv series/anime? It would break all the other episodes if we rename the whole Show folder and/or the Season folder. I understand is temporary, so it might actually be fine.

from what I've seen, I can still continue from where I paused before the whole plexdance 🤷‍♂️ even tried vinland saga

But yeah, have to sleep, dying, will write again tomorrow
Good night!

@Deathproof76
Copy link
Author

Deathproof76 commented Sep 2, 2023

Haven't found a way via plexapi so far but I can confirm that everything works out reproducibly, for single files (maybe as precaution instead of whole folder?) without breaking anything when:

  1. base order after copying to cache:
/mnt/the_pool/test2/Vinland Saga/Season 1/Vinland Saga - S01E01 - Somewhere not here (tt10233448).mkv
/mnt/cache/test2/Vinland Saga/Season 1/Vinland Saga - S01E01 - Somewhere not here (tt10233448).mkv
  1. renaming /mnt/the_pool/test2/Vinland Saga/Season 1/Vinland Saga - S01E01 - Somewhere not here (tt10233448).mkv to
    /mnt/the_pool/the_pool/Vinland Saga/Season 1/-Vinland Saga - S01E01 - Somewhere not here (tt10233448).mkv and doing a scan => same order, ondeck intact, playback continuation from hddfile, same position

  2. renaming /mnt/the_pool/test2/Vinland Saga/Season 1/-Vinland Saga - S01E01 - Somewhere not here (tt10233448).mkv back to
    /mnt/the_pool/the_pool/Vinland Saga/Season 1/Vinland Saga - S01E01 - Somewhere not here (tt10233448).mkv and doing a scan => the cache is now first in position for this single episode, ondeck intact, playback continuation from cache file, same position

so yeah, for a first working prototype script and without a more elegant solution in sight right now, that seems to be it. I think that plex has some awareness and special handling of minimal changes to filenames. The small "back and forth" thing seems necessary to break things only "a little bit" without breaking them fully

@Deathproof76
Copy link
Author

Deathproof76 commented Sep 2, 2023

I've asked a little bit with chatgpt, haven't tested yet but will test and confirm later. It aims to do the "copy to cache rerename dance" for all users. Just writing now mainly because I don't know if I have the time later. Maybe it helps or you can already see problems with this if you're interested:

import os
import shutil
from plexapi.server import PlexServer

# Plex server details
PLEX_URL = 'http:https://YOUR_PLEX_SERVER_IP:32400'
PLEX_TOKEN = 'YOUR_PLEX_TOKEN'
TARGET_MOUNT = '/path/to/target/mount'
SOURCE_PREFIX = '/mnt/the_pool/test2'
RENAME_PREFIX = '-'

# Connect to the Plex server
plex = PlexServer(PLEX_URL, PLEX_TOKEN)

# Retrieve all users
users = plex.myPlexAccount().users()

for user in users:
    # Fetch the watchlist for each user
    watchlist = user.watchlist()
    
    for item in watchlist:
        # Calculate source and target paths
        source_path = item.media[0].parts[0].file
        relative_path = os.path.relpath(source_path, SOURCE_PREFIX)
        target_path = os.path.join(TARGET_MOUNT, relative_path)
        
        # Create target directories if they don't exist
        os.makedirs(os.path.dirname(target_path), exist_ok=True)
        
        # Copy file
        shutil.copy2(source_path, target_path)

        # Update library after copying
        item.library.update()

        # Rename source file
        os.rename(source_path, source_path.replace('/', '/'+RENAME_PREFIX, 1))
        
        # Update library after renaming
        item.library.update()
        
        # Rename source file back to its original name
        renamed_source_path = source_path.replace('/', '/'+RENAME_PREFIX, 1)
        os.rename(renamed_source_path, source_path)
        
        # Scan library again
        item.library.update()

@Deathproof76
Copy link
Author

Deathproof76 commented Sep 2, 2023

maybe this ?

import os
import shutil
from plexapi.server import PlexServer

# Plex server details
PLEX_URL = 'http:https://YOUR_PLEX_SERVER_IP:32400'
PLEX_TOKEN = 'YOUR_PLEX_TOKEN'
TARGET_MOUNT = '/path/to/target/mount'
SOURCE_PREFIXES = ['/mnt/the_pool/movies', '/mnt/the_pool/tv_shows']  # Add more as needed
RENAME_PREFIX = '-'

# Connect to the Plex server
plex = PlexServer(PLEX_URL, PLEX_TOKEN)

# Retrieve all users
users = plex.myPlexAccount().users()

for user in users:
    # Fetch the watchlist for each user
    watchlist = user.watchlist()
    
    for item in watchlist:
        # Calculate source path
        source_path = item.media[0].parts[0].file
        
        # Identify which source prefix the item belongs to
        current_source_prefix = next((prefix for prefix in SOURCE_PREFIXES if source_path.startswith(prefix)), None)
        
        if not current_source_prefix:
            print(f"Unable to determine source prefix for {source_path}. Skipping.")
            continue

        # Calculate the target path while maintaining the folder structure
        relative_to_mount = source_path[len(current_source_prefix):]
        target_path = os.path.join(TARGET_MOUNT, current_source_prefix.lstrip('/'), relative_to_mount)
        
        # Create target directories if they don't exist
        os.makedirs(os.path.dirname(target_path), exist_ok=True)
        
        # Copy file
        shutil.copy2(source_path, target_path)

        # Update library after copying
        item.library.update()

        # Rename source file
        os.rename(source_path, source_path.replace('/', '/'+RENAME_PREFIX, 1))
        
        # Update library after renaming
        item.library.update()
        
        # Rename source file back to its original name
        renamed_source_path = source_path.replace('/', '/'+RENAME_PREFIX, 1)
        os.rename(renamed_source_path, source_path)
        
        # Scan library again
        item.library.update()

@bexem
Copy link
Owner

bexem commented Sep 2, 2023

Oh wow, that was a journey!
So judging your last script, it seems to do what you were basically doing manually last night. Have you tested it at all?
Sorry being brief but I will have more time to contribute later on tonight.
If it works, I could definitely try and implement it this logic in my script so that would also cover your use case!

@Deathproof76
Copy link
Author

Deathproof76 commented Sep 2, 2023

I kinda got carried away trying to get a full script running with help from chatgpt. But I'm stuck. I'll try to show you.

It should offer crontab support and notifications via apprise

pip install python-crontab
pip install apprise

There's an interactive setup.py that produces a config.py:

import os
from plexapi.server import PlexServer
import getpass
from crontab import CronTab

# Prompt for Plex server details
plex_url = input("Enter your Plex server URL (e.g., http:https://192.168.1.10:32400): ")
plex_token = input("Enter your Plex server token: ")

# Connect to the Plex server
plex = PlexServer(plex_url, plex_token)

# Fetch available libraries and let user select which ones to cache
print("\nAvailable Libraries:")
libraries = plex.library.sections()
for idx, library in enumerate(libraries, 1):
    print(f"{idx}. {library.title}")

selected_libraries = input("Enter the numbers of the libraries you want to cache (comma-separated, e.g., 1,3): ").split(',')
selected_library_names = [libraries[int(idx)-1].title for idx in selected_libraries]
source_prefixes = [libraries[int(idx)-1].locations[0] for idx in selected_libraries]

# Fetch users and let the user decide which ones to consider
print("\nAvailable Users:")
users = plex.myPlexAccount().users()
admin_user = plex.myPlexAccount().username
print(f"1. {admin_user} (Admin)")
for idx, user in enumerate(users, 2):  # Start enumeration from 2 because admin is 1
    print(f"{idx}. {user.title}")


selected_users = input("Enter the numbers of the users you want to consider (comma-separated, or 'all' for all users): ")
if selected_users.lower() != 'all':
    selected_users = [users[int(idx)-1].title for idx in selected_users.split(',')]
else:
    selected_users = []

# Ask for other configuration details
target_mount = input("\nEnter the path to the target mount (e.g., /path/to/target/mount): ")
rename_prefix = input("Enter a prefix for renaming source files during operation (e.g., '-'): ")

# Ask if the user wants to set up a crontab job
set_cron = input("\nWould you like to set up a crontab job to run the script automatically? (yes/no): ").lower()

if set_cron == 'yes':
    # Set up a crontab job
    cron_frequency = input("How often do you want the script to run? (e.g., 'daily', 'hourly', '@reboot', or custom cron syntax): ")

    cron = CronTab(user=getpass.getuser())
    job = cron.new(command=f'python3 {os.path.join(os.getcwd(), "plex_cache_script.py")}')
    if cron_frequency.lower() in ['daily', 'hourly', '@reboot']:
        job.setall(cron_frequency)
    else:
        job.setall(cron_frequency)
    cron.write()

# Ask if the user wants to set up Apprise notifications
setup_notifications = input("\nWould you like to set up notifications via Apprise? (yes/no): ").lower()

if setup_notifications == 'yes':
    apprise_url = input("Enter your Apprise configuration URL (e.g., tgram:https://BOT_TOKEN/CHAT_ID): ")
else:
    apprise_url = ''

# Generate the config.py file
with open("config.py", "w") as f:
    f.write(f"PLEX_URL = '{plex_url}'\n")
    f.write(f"PLEX_TOKEN = '{plex_token}'\n")
    f.write(f"USERS = {selected_users}\n")
    f.write(f"TARGET_MOUNT = '{target_mount}'\n")
    f.write(f"SELECTED_LIBRARIES = {selected_library_names}\n")
    f.write(f"SOURCE_PREFIXES = {source_prefixes}\n")
    f.write(f"RENAME_PREFIX = '{rename_prefix}'\n")
    f.write(f"APPRISE_URL = '{apprise_url}'  # Update this if you change your Apprise configuration\n")

print("\nSetup complete! The config.py file has been generated. If you set up Apprise notifications, ensure the Apprise configuration in config.py is correct before running the main script.")

the resulting config.py looks like this:

PLEX_URL = 'http:https://192.168.0.200:32400'
PLEX_TOKEN = 'example54G4ggsd'
USERS = ['deathproof76']
TARGET_MOUNT = '/mnt/cache'
SELECTED_LIBRARIES = ['test1', 'test2']
SOURCE_PREFIXES = ['/mnt/the_pool/test1', '/mnt/the_pool/test2']
RENAME_PREFIX = '-'
APPRISE_URL = ''  # Update this if you change your Apprise configuration

and this is the plex_cache_script.py that uses the config.py:

import os
import time
import shutil
from plexapi.server import PlexServer
from plexapi.video import Episode, Season
import apprise
from config import PLEX_URL, PLEX_TOKEN, USERS, TARGET_MOUNT, SELECTED_LIBRARIES, SOURCE_PREFIXES, RENAME_PREFIX, APPRISE_URL

def build_target_path(source_path, source_prefix):
    """Helper function to build the target path for a given source path."""
    relative_to_mount = source_path[len(source_prefix):]
    target = os.path.join(TARGET_MOUNT, relative_to_mount.lstrip('/'))

    return target

# A set to hold all valid relative paths that should exist in the target mount after the script runs
valid_relative_paths = set()

# Connect to the Plex server
plex = PlexServer(PLEX_URL, PLEX_TOKEN)

# Apprise setup
if APPRISE_URL:
    apobj = apprise.Apprise()
    apobj.add(APPRISE_URL)

# If USERS list is not empty, filter only specified users
if USERS:
    users = [user for user in plex.myPlexAccount().users() if user.title in USERS]
else:
    users = plex.myPlexAccount().users()

# Add the admin user to the list
users.append(plex.myPlexAccount())

for user in users:
    # Fetch the watchlist for each user
    if user.title == plex.myPlexAccount().username:  # Check if the user is the admin
        watchlist = plex.library.recentlyAdded()
    else:
        user_plex = PlexServer(PLEX_URL, user.get_token(plex.machineIdentifier))
        watchlist = user_plex.library.recentlyAdded()

    for item in watchlist:
        # Check if the item's library is one of the selected libraries
        if item.librarySectionTitle not in SELECTED_LIBRARIES:
            continue

        items_to_copy = [item]

        # Check if the item is an episode and get the next five episodes
        if isinstance(item, Episode):
            season = item.season()
            episode_number = item.index  # This is the episode number within the season

            # Get all episodes of the season starting from the current episode
            following_episodes = [ep for ep in season.episodes() if ep.index > episode_number]

            items_to_copy.extend(following_episodes[:5])

            # If fewer than 5 episodes in current season, fetch from the next season
            while len(items_to_copy) < 6:
                season_number = season.index
                show = season.show()
                next_season = show.season(season_number + 1)

                # If no next season, break
                if next_season is None:
                    break

                additional_episodes_needed = 6 - len(items_to_copy)
                items_to_copy.extend(next_season.episodes()[:additional_episodes_needed])

                season = next_season

        # If the item is a season, take only the first 5 episodes
        elif isinstance(item, Season):
            items_to_copy = item.episodes()[:5]

        for item_to_copy in items_to_copy:
            # Calculate source path
            source_path = item_to_copy.media[0].parts[0].file

            # Identify which source prefix the item belongs to
            current_source_prefix = next((prefix for prefix in SOURCE_PREFIXES if source_path.startswith(prefix)), None)

            if not current_source_prefix:
                print(f"Unable to determine source prefix for {source_path}. Skipping.")
                continue

            # Calculate the target path while maintaining the folder structure
            target_path = build_target_path(source_path, current_source_prefix)

            # Add to valid paths set
            relative_path = os.path.relpath(source_path, current_source_prefix)
            valid_relative_paths.add(relative_path)

            # Create target directories if they don't exist
            print(f"Source Path: {source_path}")
            print(f"Source Prefix: {current_source_prefix}")
            print(f"Target Path: {target_path}")
            os.makedirs(os.path.dirname(target_path), exist_ok=True)

            # Copy file
            shutil.copy2(source_path, target_path)

            # Scan library after copying
            library_section_id = None
            if isinstance(item_to_copy, Episode):
                library_section_id = item_to_copy.show().librarySectionID
            else:
                library_section_id = item_to_copy.librarySectionID

            library_to_update = plex.library.sectionByID(library_section_id)
            library_to_update.update()

# Copy associated files (subtitles, theme music, trailers, etc.)
source_dir = os.path.dirname(source_path)
for associated_file in os.listdir(source_dir):
    if associated_file.endswith(('.srt', '.mp3', '-trailer.mp4')):  # Add other extensions if needed
        associated_source_path = os.path.join(source_dir, associated_file)
        associated_target_path = build_target_path(associated_source_path, current_source_prefix)

                    # Add to valid paths set
        associated_relative_path = os.path.relpath(associated_source_path, current_source_prefix)
        valid_relative_paths.add(associated_relative_path)

        shutil.copy2(associated_source_path, associated_target_path)

            # Update library after copying
        library_section_id = None
        if isinstance(item_to_copy, Episode):
            library_section_id = item_to_copy.show().librarySectionID
        else:
            library_section_id = item_to_copy.librarySectionID

        library_to_update = plex.library.sectionByID(library_section_id)
        library_to_update.update()
        
        time.sleep(10)

        # Rename source file
        dirname, filename = os.path.split(source_path)
        new_path = os.path.join(dirname, RENAME_PREFIX + filename)
        os.rename(source_path, new_path)


            # Update library after renaming
        library_section_id = None
        if isinstance(item_to_copy, Episode):
            library_section_id = item_to_copy.show().librarySectionID
        else:
            library_section_id = item_to_copy.librarySectionID

        library_to_update = plex.library.sectionByID(library_section_id)
        library_to_update.update()
        
        time.sleep(10)
        # Rename source file back to its original name
        renamed_source_path = source_path.replace('/', '/'+RENAME_PREFIX, 1)
        os.rename(renamed_source_path, source_path)

            # Scan library again
        library_section_id = None
        if isinstance(item_to_copy, Episode):
            library_section_id = item_to_copy.show().librarySectionID
        else:
            library_section_id = item_to_copy.librarySectionID

        library_to_update = plex.library.sectionByID(library_section_id)
        library_to_update.update()
        
        time.sleep(10)

            # Now, remove files/folders from the target mount that are not in valid_relative_paths
        for root, dirs, files in os.walk(TARGET_MOUNT, topdown=False):  # topdown=False to ensure we delete children before parents
            for name in files:
                file_path = os.path.join(root, name)
                relative_file_path = os.path.relpath(file_path, TARGET_MOUNT)
                if relative_file_path not in valid_relative_paths:
                    os.remove(file_path)

        for name in dirs:
            dir_path = os.path.join(root, name)
            if not os.listdir(dir_path):
                os.rmdir(dir_path)

        # Only send a notification if APPRISE_URL is configured
        if APPRISE_URL:
            apobj.notify(
                title='Plex Cache Update Complete',
                body='Plex cache script has completed its run.'
            )

There are currently two problems, I could identify so far:
first, for a show, instead of copying the five episodes followin the current ondeck from /mnt/the_pool/test2/Cool TV Show(tt0108783)/Season 1/* to /mnt/cache/the_pool/test2/Cool TV Show(tt0108783)/Season 1/* they get copied to /mnt/cache/Cool TV Show(tt0108783)/Season 1/*

second, the copying source files to cache -> library scan -> rename files in source -> library scan -> rerename files back ->library scan seems to happen too fast. so that basically the first scan isn't even done while the source files have already been renamed back to the original name. Somehow the time.sleep(10) seems to get ignored and the rename-scan-dance doesn't work like it would manually.

Yeah, I'm currently out of my depth on this. Need to understand a lot more first. But I hope that this can help you in some way, for a proper solution 😅👍 I'll be trying again tomorrow. I really should've started with the basics first

@bexem
Copy link
Owner

bexem commented Sep 2, 2023

You've done a lot! I was expecting you to try and implement your logic into my script so that you had most of the work done. But either way I might have noticed when the problem is with the function build_target_path, the function was stripping away the source prefix entirely, leaving just the relative path of the file, and then it was joining this with TARGET_MOUNT. It didn't account for retaining the test1 or test2 directory, which led to the omission in the target path.

def build_target_path(source_path, source_prefix):
    """Helper function to build the target path for a given source path."""
    relative_to_mount = source_path[len(source_prefix):]
    target_prefix = source_prefix.split('/')[-1]  # Extract the last part of the source prefix
    target = os.path.join(TARGET_MOUNT, target_prefix, relative_to_mount.lstrip('/'))
    return target

About time.sleep(10), you are not using any multithreading so it definitely should work. But I noticed in the plexapi documentation you can check if a library is currently refreshing, you could use that to wait the right time before proceeding:

        # Wait until library scan is completed
        while any(library.refreshing for library in library_to_update):
            print("Server is currently scanning libraries. Waiting...")
            time.sleep(10)  # Wait for 10 seconds before checking again

@Deathproof76
Copy link
Author

Deathproof76 commented Sep 2, 2023

I mean, have you seen your 1383 lines script?😄 It's kinda overwhelming for a low-level script noob. I just hope that I get at least something to work, then I'll look into that whole integrating thing

So I modified my wonky script with your suggestions and the files in question get copied to /mnt/cache/test2/Gargoyles (tt0108783)/Season 1/Gargoyles - S01E02 - Awakening (2).mkv this time ,not with the /mnt/cache/the_pool/test2/ , but that's actually fine for what I need because the mediafolders have unique names anyways. But there seems to be something wrong with the rename-scan-dance logic. Maybe wrong sequence? It actually takes a long time per file now and looks like it's doing a scan every file. And I couldn't find any indication for the file name changes being registered in the plex logs (Are they even being made?). Will have to try further tomorrow. Going to start by stripping down to only change the position of default play with same files in two folders. Which is what I really should have done first...

@bexem
Copy link
Owner

bexem commented Sep 2, 2023

I mean, have you seen your 1383 lines script?😄 It's kinda overwhelming for a low-level script noob. I just hope that I get at least something to work, then I'll look into that whole integrating thing

Ahahah fair point!! It used to be a short and simple script, now I get lost every time I don't look at it for a couple of days!

But there seems to be something wrong with the rename-scan-dance logic. Maybe wrong sequence? It actually takes a long time per file now and looks like it's doing a scan every file. And I couldn't find any indication for the file name changes being registered in the plex logs (Are they even being made?).

Instead of refreshing the whole library, might be worth ask plex to check for changes just in the path, might shave some time off, if it actually work as apparently is not??
For example:

# Update library after copying
if isinstance(item_to_copy, Episode):
    library_section_id = item_to_copy.show().librarySectionID
    specific_path_to_update = os.path.dirname(target_path)  # The directory where the media was copied
else:
    library_section_id = item_to_copy.librarySectionID
    specific_path_to_update = os.path.dirname(target_path)  # The directory where the media was copied

library_to_update = plex.library.sectionByID(library_section_id)
library_to_update.update(path=specific_path_to_update)

@Deathproof76
Copy link
Author

Deathproof76 commented Sep 3, 2023

Got it to work! Just a super simple static script focused on the rename-scan-dance to change the order. The trick was 10 sec sleeping directly after rename, but before scanning. For good measure the same thing with sleep after renaming back to original. Assumption is that the files have already been duplicated: one in cache, one in hdd. The files getting the rename-scan-dance will always be positioned last. Tested it on a movie library and also with tv shows:

config.py

# Configuration for Plex server
PLEX_URL = '192.168.0.200:32400'
PLEX_TOKEN = 'example4g4T4Y'
PLEX_LIBRARY_NAME = 'test2'  # The name of your Plex library

# Folders
RENAME_FOLDER = '/mnt/the_pool/test2' # folder of the library
IGNORE_FOLDER_PREFIX = '/mnt/cache/' # cache folder

plex_rename_scan.py

import os
import time
from plexapi.server import PlexServer
from config import PLEX_URL, PLEX_TOKEN, PLEX_LIBRARY_NAME, RENAME_FOLDER, IGNORE_FOLDER_PREFIX

BASE_URL = f'http:https://{PLEX_URL}'
server = PlexServer(BASE_URL, PLEX_TOKEN)
library = server.library.section(PLEX_LIBRARY_NAME)


def rename_files(directory, revert=False):
    for root, dirs, files in os.walk(directory):
        for file in files:
            # Check if the path is not ignored
            if not root.startswith(IGNORE_FOLDER_PREFIX):
                old_path = os.path.join(root, file)
                if revert:
                    # Revert renaming
                    if file.startswith("-"):
                        new_name = file[1:]
                        new_path = os.path.join(root, new_name)
                        os.rename(old_path, new_path)
                else:
                    # Rename the files
                    new_name = "-" + file
                    new_path = os.path.join(root, new_name)
                    os.rename(old_path, new_path)


def wait_for_scan_completion(library_to_update):
    # Wait until library scan is completed
    while any(library.refreshing for library in library_to_update):
        print("Server is currently scanning libraries. Waiting...")
        time.sleep(10)  # Wait for 10 seconds before checking again


def main():
    # Rename files
    rename_files(RENAME_FOLDER)
    time.sleep(10)  # Wait for 10 seconds after renaming

    # Scan library
    library.update()
    wait_for_scan_completion([library])
    time.sleep(10)  # Wait for 10 seconds after the first scan

    # Revert renaming
    rename_files(RENAME_FOLDER, revert=True)
    time.sleep(10)  # Wait for 10 seconds after renaming back

    # Scan library again
    library.update()
    wait_for_scan_completion([library])

    print("Process completed!")


if __name__ == "__main__":
    main()

Ondeck/continue works. Tested a few different scenarios and only thing I noticed: when the cache media is in first position and the file is deleted (which for example reflects the case, when media has already been seen and cache can be freed) , the media will be marked as unavailable even the other version can be chosen and played. The library need to be rescanned and trash needs to be emptied, but it didn't update as fast as I would have wished, took a few tries to completly loose the cache path and have the hdd file being the only one. Plex seems to have some preventative measure for drive disconnects or media corruption that might play into this.

Another solution for this might be a reverse rename-scan-dance where the files in the cache get this treatment before being deleted to make space for new files to be cached.

I'll be looking further, it may also make sense to exclude the files instead of renaming. You know for the first step when moving files to cache just to be absolutely sure, that it get's played from cache. Also metafiles like theme.mp3, may need the same treatment to guarantee that plex doesn't wake the hdd (Have seen that you thought about subtitles too).

@defract
Copy link

defract commented Apr 12, 2024

@Deathproof76 I wonder if you settled on what you wrote last, or if you tried other things (like plexignore)?

Kind of wanted to do the same (prefer files on cache instead of array) and noticed all the same problems you did (Plex only cares about the order of discovery of media and so on).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants