Stage new submodule locations
This commit is contained in:
4
submodules/mpv-anilist-updater/.gitignore
vendored
Normal file
4
submodules/mpv-anilist-updater/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
anilistToken.txt
|
||||
.pylintrc
|
||||
pyrightconfig.json
|
||||
.vscode
|
||||
21
submodules/mpv-anilist-updater/LICENSE
Normal file
21
submodules/mpv-anilist-updater/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 AzuredBlue
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
166
submodules/mpv-anilist-updater/README.md
Normal file
166
submodules/mpv-anilist-updater/README.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# mpv-anilist-updater
|
||||
|
||||
A script for MPV that automatically updates your AniList based on the file you just watched.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> By default, the anime must be set to "watching", "planning" or "rewatching" to update progress. This is done in order to prevent updating the wrong show.<br>
|
||||
> **Recommendation:** Check out the configuration options in your `anilistUpdater.conf` file to customize the script to your needs. See the [Configuration](#configuration-anilistupdaterconf) section for details.<br>
|
||||
|
||||
> [!TIP]
|
||||
> In order for the script to work properly, make sure your files are named correctly:<br>
|
||||
>
|
||||
> - Either the file or folder its in must have the anime title in it<br>
|
||||
> - The file must have the episode number in it (absolute numbering should work)<br>
|
||||
> - In case of remakes, specify the year of the remake to ensure it updates the proper one<br>
|
||||
>
|
||||
> To avoid the script running and making useless API calls, you can set one or more directories in the config file. See the [Configuration](#configuration-anilistupdaterconf) section below.
|
||||
|
||||
For any issues, you can either open an issue on here, or message me on discord (azuredblue)
|
||||
|
||||
## Requirements
|
||||
|
||||
You will need Python 3 installed, as well as the libraries `guessit` and `requests`:
|
||||
|
||||
```bash
|
||||
pip install guessit requests
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Simply download the `anilistUpdater` folder and put it in your mpv scripts folder, or download the contents and make the folder yourself.
|
||||
|
||||
You **WILL** need an AniList access token for it to work:
|
||||
|
||||
1. Visit `https://anilist.co/api/v2/oauth/authorize?client_id=20740&response_type=token`
|
||||
2. Authorize the app
|
||||
3. Copy the token
|
||||
4. Create an `anilistToken.txt` file in the `anilistUpdater` folder (if not already there) and paste the token there.
|
||||
|
||||
This .txt file is also used to cache your AniList user id and to cache recently seen shows, avoiding extra API Calls.
|
||||
This token is what allows the script to update the anime episode count and make api requests, it is not used for anything else.
|
||||
|
||||
## Configuration (`anilistUpdater.conf`)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The config file is only generated after you run mpv at least once with the script installed. If the file is not created due to lack of write permissions, you can either run mpv as administrator once to generate it, or create the file manually in the appropriate directory. It is recommended to be in the `script-opts` directory.
|
||||
|
||||
When you first run the script, it will automatically create a configuration file called `anilistUpdater.conf` if it does not already exist. This file is typically created in your mpv `script-opts` directory where you have mpv installed. If it cannot be created there, it will try the `scripts` directory or your mpv config directory (e.g. `~/.config/mpv/` on Linux, `%APPDATA%/mpv/` on Windows).
|
||||
|
||||
**Config file search order:**
|
||||
|
||||
The script checks for the config file in the following order:
|
||||
|
||||
1. `script-opts` directory (recommended)
|
||||
2. `scripts` directory (where the script itself is located)
|
||||
3. mpv config directory (e.g. `~/.config/mpv/` or `%APPDATA%/mpv/`)
|
||||
|
||||
If the config file exists in more than one location, the one in the highest-priority directory (script-opts) will be used. For example, if you have a config in both `script-opts` and `scripts`, the one in `script-opts` will take precedence.
|
||||
|
||||
**You should edit this file to change any options.**
|
||||
|
||||
### Example `anilistUpdater.conf`
|
||||
|
||||
```ini
|
||||
# Use 'yes' or 'no' for boolean options below
|
||||
# Example for multiple directories (comma or semicolon separated):
|
||||
# DIRECTORIES=D:/Torrents,D:/Anime
|
||||
# or
|
||||
# DIRECTORIES=D:/Torrents;D:/Anime
|
||||
DIRECTORIES=
|
||||
UPDATE_PERCENTAGE=85
|
||||
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE=no
|
||||
UPDATE_PROGRESS_WHEN_REWATCHING=yes
|
||||
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT=yes
|
||||
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING=yes
|
||||
```
|
||||
|
||||
#### Option Descriptions
|
||||
|
||||
- **DIRECTORIES**: Comma or semicolon separated list of directories. If empty, the script works for every video. Example: `DIRECTORIES=D:/Torrents,D:/Anime`
|
||||
- Restricting directories only prevents the script from automatically updating AniList for files outside the specified directories. Manual actions using the keybinds (Ctrl+A, Ctrl+B, Ctrl+D) will still work for any file, regardless of its location.
|
||||
- **UPDATE_PERCENTAGE**: Number (0-100). The percentage of the video you need to watch before it updates AniList automatically. Default is `85`.
|
||||
- **SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE**: `yes`/`no`. If `yes`, when watching episode 1 of a completed anime, set it to rewatching and update progress. Default is `no`.
|
||||
- **UPDATE_PROGRESS_WHEN_REWATCHING**: `yes`/`no`. If `yes`, allow updating progress for anime set to rewatching. Default is `yes`.
|
||||
- **SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT**: `yes`/`no`. If `yes`, set to COMPLETED after last episode if status was CURRENT. **Default is `yes`.**
|
||||
- **SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING**: `yes`/`no`. If `yes`, set to COMPLETED after last episode if status was REPEATING (rewatching). Default is `yes`.
|
||||
|
||||
> [!NOTE]
|
||||
> All boolean options must be `yes` or `no` (not `true`/`false`).
|
||||
|
||||
## Usage
|
||||
|
||||
This script has 3 keybinds:
|
||||
|
||||
- Ctrl + A: Manually updates your AniList with the current episode you are watching.
|
||||
- Ctrl + B: Opens the AniList page of the anime you are watching on your browser. Useful to see if it guessed the anime correctly.
|
||||
- Ctrl + D: Opens the folder where the current video is playing. Useful if you have "your own" anime library, and navigating through folders is a pain.
|
||||
|
||||
The script will automatically update your AniList when the video you are watching reaches 85% completion (or the percentage you set in the config file).
|
||||
|
||||
You can change the keybinds in your input.conf:
|
||||
|
||||
```bash
|
||||
A script-binding update_anilist
|
||||
B script-binding launch_anilist
|
||||
D script-binding open_folder
|
||||
```
|
||||
|
||||
Or in the `.lua` file:
|
||||
|
||||
```lua
|
||||
mp.add_key_binding('ctrl+a', 'update_anilist', function()
|
||||
update_anilist("update")
|
||||
end)
|
||||
|
||||
mp.add_key_binding('ctrl+b', 'launch_anilist', function()
|
||||
update_anilist("launch")
|
||||
end)
|
||||
|
||||
mp.add_key_binding('ctrl+d', 'open_folder', open_folder)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The script uses Guessit to try to get as much information as possible from the file name.
|
||||
|
||||
If the "episode" and "season" guess are before the title, it will consider that title wrong and try to get the title from the name of the folder it is in.
|
||||
|
||||
If the torrent file has absolute numbering (looking at you, SubsPlease), it will try to guess the season and episode by:
|
||||
|
||||
1. Searching for the anime name on the AniList API.
|
||||
2. Get all results with a similar name, whose format are `TV` and the duration greater than 21 minutes.
|
||||
3. Sort them based on release date.
|
||||
4. Get the season based on the absolute episode number
|
||||
|
||||
It is not a flawless method. It won't work properly if the anime has seasons as ONA's. If it doesn't work properly, consider
|
||||
changing the episode number to the normal format yourself, or simply give up on that series.
|
||||
|
||||
## FAQ (Probably)
|
||||
|
||||
**Q: On what formats does it work?**
|
||||
|
||||
A: It should work on most formats as long as the name is present in the file itself or the folder name.
|
||||
|
||||
`[SubsPlease] Boku no Hero Academia - 152 (1080p) [AAC292E7].mkv` will be detected as S7 E14 and updated accordingly.
|
||||
|
||||
`E12 - Welcome [F1119374].mkv` will work if the folder that it is in has `86` in the name. If it has `86 Part 2` then it should be `Episode 1`
|
||||
|
||||
If it does not, try changing the name of the file / folder, so the search has a better chance at finding it
|
||||
|
||||
**Q: Can I see which anime got detected before it updates?**
|
||||
|
||||
A: Ctrl + B will launch the AniList page of the anime it detects. To see more debug info launch via command line with `mpv file.mkv` or press the `` ` `` keybind to open the console.
|
||||
|
||||
**Q: Can it wrongfully update my anime?**
|
||||
|
||||
A: No, AniList's API does not allow updating an anime which isnt on your watch list. If it didn't detect your anime correctly, then it will
|
||||
simply error.
|
||||
|
||||
**Q: It does not work with X format. What do I do?**
|
||||
|
||||
A: You can try launching the file through the command line with `mpv file.mkv` or opening the console through the keybind \` and see `Guessed name: X`. Try changing the file's name or folder so it has
|
||||
a better chance at guessing the anime. If it still doesn't work, try opening a GitHub issue or messaging me on discord (azuredblue).
|
||||
|
||||
## Credits
|
||||
|
||||
This script was inspired by [mpv-open-anilist-page](https://github.com/ehoneyse/mpv-open-anilist-page) by ehoneyse.
|
||||
727
submodules/mpv-anilist-updater/anilistUpdater/anilistUpdater.py
Normal file
727
submodules/mpv-anilist-updater/anilistUpdater/anilistUpdater.py
Normal file
@@ -0,0 +1,727 @@
|
||||
"""
|
||||
mpv-anilist-updater: Automatically updates your AniList based on the file you just watched in MPV.
|
||||
|
||||
This script parses anime filenames, determines the correct AniList entry, and updates your progress
|
||||
or status accordingly.
|
||||
"""
|
||||
|
||||
# Configuration options for anilistUpdater (set in anilistUpdater.conf):
|
||||
#
|
||||
# DIRECTORIES: List or comma/semicolon-separated string. The directories the script will work on. Leaving it empty will make it work on every video you watch with mpv. Example: DIRECTORIES = ["D:/Torrents", "D:/Anime"]
|
||||
#
|
||||
# UPDATE_PERCENTAGE: Integer (0-100). The percentage of the video you need to watch before it updates AniList automatically. Default is 85 (usually before the ED of a usual episode duration).
|
||||
#
|
||||
# SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE: Boolean. If true, when watching episode 1 of a completed anime, set it to rewatching and update progress.
|
||||
#
|
||||
# UPDATE_PROGRESS_WHEN_REWATCHING: Boolean. If true, allow updating progress for anime set to rewatching. This is for if you want to set anime to rewatching manually, but still update progress automatically.
|
||||
#
|
||||
# SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT: Boolean. If true, set to COMPLETED after last episode if status was CURRENT.
|
||||
#
|
||||
# SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING: Boolean. If true, set to COMPLETED after last episode if status was REPEATING (rewatching).
|
||||
|
||||
import sys
|
||||
import os
|
||||
import webbrowser
|
||||
import time
|
||||
import ast
|
||||
import hashlib
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
from guessit import guessit
|
||||
|
||||
class AniListUpdater:
|
||||
"""
|
||||
Handles AniList authentication, file parsing, API requests, and updating anime progress/status.
|
||||
"""
|
||||
ANILIST_API_URL = 'https://graphql.anilist.co'
|
||||
TOKEN_PATH = os.path.join(os.path.dirname(__file__), 'anilistToken.txt')
|
||||
OPTIONS = "--excludes country --excludes language --type episode"
|
||||
CACHE_REFRESH_RATE = 24 * 60 * 60
|
||||
|
||||
# Load token and user id
|
||||
def __init__(self, options):
|
||||
"""
|
||||
Initializes the AniListUpdater, loading the access token and user ID.
|
||||
"""
|
||||
self.access_token = self.load_access_token() # Replace token here if you don't use the .txt
|
||||
self.user_id = self.get_user_id()
|
||||
self.options = options
|
||||
|
||||
# Load token from anilistToken.txt
|
||||
def load_access_token(self):
|
||||
"""
|
||||
Loads the AniList access token from the token file.
|
||||
Returns:
|
||||
str or None: The access token, or None if not found.
|
||||
"""
|
||||
try:
|
||||
with open(self.TOKEN_PATH, 'r', encoding='utf-8') as file:
|
||||
content = file.read().strip()
|
||||
if ':' in content:
|
||||
token = content.split(':', 1)[1].splitlines()[0]
|
||||
return token
|
||||
|
||||
return content
|
||||
except Exception as e:
|
||||
print(f'Error reading access token: {e}')
|
||||
return None
|
||||
|
||||
# Load user id from file, if not then make api request and save it.
|
||||
def get_user_id(self):
|
||||
"""
|
||||
Loads the AniList user ID from the token file, or fetches and caches it if not present.
|
||||
Returns:
|
||||
int or None: The user ID, or None if not found.
|
||||
"""
|
||||
try:
|
||||
with open(self.TOKEN_PATH, 'r', encoding='utf-8') as file:
|
||||
content = file.read().strip()
|
||||
if ':' in content:
|
||||
return int(content.split(':')[0])
|
||||
except Exception as e:
|
||||
print(f'Error reading user ID: {e}')
|
||||
|
||||
query = '''
|
||||
query {
|
||||
Viewer {
|
||||
id
|
||||
}
|
||||
}
|
||||
'''
|
||||
response = self.make_api_request(query, None, self.access_token)
|
||||
if response and 'data' in response:
|
||||
user_id = response['data']['Viewer']['id']
|
||||
self.save_user_id(user_id)
|
||||
return user_id
|
||||
return None
|
||||
|
||||
# Cache user id
|
||||
def save_user_id(self, user_id):
|
||||
"""
|
||||
Saves the user ID to the token file, prepending it to the existing content.
|
||||
Args:
|
||||
user_id (int): The AniList user ID.
|
||||
"""
|
||||
try:
|
||||
with open(self.TOKEN_PATH, 'r+', encoding='utf-8') as file:
|
||||
content = file.read()
|
||||
file.seek(0)
|
||||
file.write(f'{user_id}:{content}')
|
||||
except Exception as e:
|
||||
print(f'Error saving user ID: {e}')
|
||||
|
||||
def cache_to_file(self, path, guessed_name, result):
|
||||
"""
|
||||
Appends a cache entry to the token file for a given file path and guessed anime name.
|
||||
Args:
|
||||
path (str): The file path.
|
||||
guessed_name (str): The guessed anime name.
|
||||
result (tuple): The result to cache.
|
||||
"""
|
||||
try:
|
||||
with open(self.TOKEN_PATH, 'a', encoding='utf-8') as file:
|
||||
# Epoch Time, hash of the path, guessed name, result
|
||||
file.write(f'\n{time.time()};;{self.hash_path(os.path.dirname(path))};;{guessed_name};;{result}')
|
||||
except Exception as e:
|
||||
print(f'Error trying to cache {result}: {e}')
|
||||
|
||||
def hash_path(self, path):
|
||||
"""
|
||||
Returns a SHA256 hash of the given path.
|
||||
Args:
|
||||
path (str): The path to hash.
|
||||
Returns:
|
||||
str: The hashed path.
|
||||
"""
|
||||
return hashlib.sha256(path.encode('utf-8')).hexdigest()
|
||||
|
||||
def check_and_clean_cache(self, path, guessed_name):
|
||||
"""
|
||||
Checks the cache for a matching entry and cleans out expired entries.
|
||||
Args:
|
||||
path (str): The file path.
|
||||
guessed_name (str): The guessed anime name.
|
||||
Returns:
|
||||
tuple: (cached_result, line_index) or (None, None) if not found.
|
||||
"""
|
||||
try:
|
||||
valid_lines = []
|
||||
unique = set()
|
||||
path = self.hash_path(os.path.dirname(path))
|
||||
cached_result = (None, None)
|
||||
|
||||
with open(self.TOKEN_PATH, 'r+', encoding='utf-8') as file:
|
||||
orig_lines = file.readlines()
|
||||
|
||||
for line in orig_lines:
|
||||
if line.strip():
|
||||
if ';;' in line:
|
||||
epoch, dir_path, guess, result = line.strip().split(';;')
|
||||
|
||||
if time.time() - float(epoch) < self.CACHE_REFRESH_RATE and (dir_path, guess) not in unique:
|
||||
unique.add((dir_path, guess))
|
||||
valid_lines.append(line)
|
||||
|
||||
if dir_path == path and guess == guessed_name:
|
||||
cached_result = (result, len(valid_lines) - 1)
|
||||
else:
|
||||
valid_lines.append(line)
|
||||
|
||||
if valid_lines != orig_lines:
|
||||
with open(self.TOKEN_PATH, 'w', encoding='utf-8') as file:
|
||||
file.writelines(valid_lines)
|
||||
|
||||
return cached_result
|
||||
except Exception as e:
|
||||
print(f'Error trying to read cache file: {e}')
|
||||
|
||||
def update_cache(self, path, guessed_name, result, index):
|
||||
"""
|
||||
Updates a cache entry at the given index with new data.
|
||||
Args:
|
||||
path (str): The file path.
|
||||
guessed_name (str): The guessed anime name.
|
||||
result (tuple): The result to cache.
|
||||
index (int): The line index in the cache file.
|
||||
"""
|
||||
try:
|
||||
with open(self.TOKEN_PATH, 'r', encoding='utf-8') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
if 0 <= index < len(lines):
|
||||
# Update the line at the given index with the new cache data
|
||||
updated_line = f'{time.time()};;{self.hash_path(os.path.dirname(path))};;{guessed_name};;{result}\n' if result is not None else ''
|
||||
lines[index] = updated_line
|
||||
|
||||
# Write the updated lines back to the file
|
||||
with open(self.TOKEN_PATH, 'w', encoding='utf-8') as file:
|
||||
file.writelines(lines)
|
||||
|
||||
else:
|
||||
print(f"Invalid index {index} for updating cache.")
|
||||
except Exception as e:
|
||||
print(f'Error trying to update cache file: {e}')
|
||||
|
||||
# Function to make an api request to AniList's api
|
||||
def make_api_request(self, query, variables=None, access_token=None):
|
||||
"""
|
||||
Makes a POST request to the AniList GraphQL API.
|
||||
Args:
|
||||
query (str): The GraphQL query string.
|
||||
variables (dict, optional): Variables for the query.
|
||||
access_token (str, optional): AniList access token.
|
||||
Returns:
|
||||
dict or None: The API response as a dict, or None on error.
|
||||
"""
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
if access_token:
|
||||
headers['Authorization'] = f'Bearer {access_token}'
|
||||
|
||||
response = requests.post(self.ANILIST_API_URL, json={'query': query, 'variables': variables}, headers=headers, timeout=10)
|
||||
# print(f"Made an API Query with: Query: {query}\nVariables: {variables} ")
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
print(f'API request failed: {response.status_code} - {response.text}\nQuery: {query}\nVariables: {variables}')
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def season_order(season):
|
||||
"""
|
||||
Returns a numeric order for seasons for sorting.
|
||||
Args:
|
||||
season (str): The season name (WINTER, SPRING, SUMMER, FALL).
|
||||
Returns:
|
||||
int: The order value.
|
||||
"""
|
||||
return {'WINTER': 1, 'SPRING': 2, 'SUMMER': 3, 'FALL': 4}.get(season, 5)
|
||||
|
||||
def filter_valid_seasons(self, seasons):
|
||||
"""
|
||||
Filters and sorts valid TV seasons for absolute numbering logic.
|
||||
Args:
|
||||
seasons (list): List of season dicts from AniList API.
|
||||
Returns:
|
||||
list: Filtered and sorted list of seasons.
|
||||
"""
|
||||
# Filter only to those whose format is TV and duration > 21 OR those who have no duration and are releasing.
|
||||
# This is due to newly added anime having duration as null
|
||||
seasons = [
|
||||
season for season in seasons
|
||||
if ((season['duration'] is None and season['status'] == 'RELEASING') or
|
||||
(season['duration'] is not None and season['duration'] > 21)) and season['format'] == 'TV'
|
||||
]
|
||||
# One of the problems with this filter is needing the format to be 'TV'
|
||||
# But if accepted any format, it would also include many ONA's which arent included in absolute numbering.
|
||||
|
||||
# Sort them based on release date
|
||||
seasons = sorted(seasons, key=lambda x: (x['seasonYear'] if x['seasonYear'] else float("inf"), self.season_order(x['season'] if x['season'] else float("inf"))))
|
||||
return seasons
|
||||
|
||||
# Finds the season and episode of an anime with absolute numbering
|
||||
def find_season_and_episode(self, seasons, absolute_episode):
|
||||
"""
|
||||
Finds the correct season and relative episode for an absolute episode number.
|
||||
Args:
|
||||
seasons (list): List of season dicts.
|
||||
absolute_episode (int): The absolute episode number.
|
||||
Returns:
|
||||
tuple: (season_id, season_title, progress, episodes, relative_episode)
|
||||
"""
|
||||
accumulated_episodes = 0
|
||||
for season in seasons:
|
||||
season_episodes = season.get('episodes', 12) if season.get('episodes') else 12
|
||||
|
||||
if accumulated_episodes + season_episodes >= absolute_episode:
|
||||
return (
|
||||
season.get('id'),
|
||||
season.get('title', {}).get('romaji'),
|
||||
season.get('mediaListEntry', {}).get('progress') if season.get('mediaListEntry') else None,
|
||||
season.get('episodes'),
|
||||
absolute_episode - accumulated_episodes
|
||||
)
|
||||
accumulated_episodes += season_episodes
|
||||
return (None, None, None, None, None)
|
||||
|
||||
def handle_filename(self, filename):
|
||||
"""
|
||||
Main entry point for handling a file: parses, checks cache, updates AniList, and manages cache.
|
||||
Args:
|
||||
filename (str): The path to the video file.
|
||||
"""
|
||||
file_info = self.parse_filename(filename)
|
||||
cached_result, line_index = self.check_and_clean_cache(filename, file_info.get('name'))
|
||||
# str -> tuple
|
||||
if cached_result:
|
||||
try:
|
||||
cached_result = ast.literal_eval(cached_result)
|
||||
if not isinstance(cached_result, (tuple, list)):
|
||||
cached_result = None
|
||||
except Exception:
|
||||
cached_result = None
|
||||
else:
|
||||
cached_result = None
|
||||
|
||||
# True if:
|
||||
# Is not cached
|
||||
# Tries to update and current episode is not the next one.
|
||||
# It is not in your watching/planning list.
|
||||
# This means that for shows with absolute numbering, if it updates, it will always call the API
|
||||
# Since it needs to convert from absolute to relative.
|
||||
if cached_result is None or (cached_result and (file_info.get('episode') != cached_result[2] + 1) and sys.argv[2] != 'launch'):
|
||||
result = self.get_anime_info_and_progress(file_info.get('name'), file_info.get('episode'), file_info.get('year'))
|
||||
result = self.update_episode_count(result) # Returns either the same, or the updated result
|
||||
|
||||
# If it returned a result and the progress isnt None, then put it in cache, since it wasn't.
|
||||
if result and result[2] is not None:
|
||||
if line_index is not None:
|
||||
print(f'Updating cache to: {result}')
|
||||
self.update_cache(filename, file_info.get('name'), result, line_index)
|
||||
else:
|
||||
print(f'Not found in cache! Adding to file... {result}')
|
||||
self.cache_to_file(filename, file_info.get('name'), result)
|
||||
|
||||
# True for opening AniList and updating next episode.
|
||||
else:
|
||||
print(f'Found in cache! {cached_result}')
|
||||
# Only proceed if cached_result is a tuple/list and has enough elements
|
||||
if isinstance(cached_result, (tuple, list)) and len(cached_result) >= 4:
|
||||
# Change to the episode that needs to be updated
|
||||
if len(cached_result) > 5:
|
||||
cached_result = tuple(cached_result[:4]) + (file_info.get('episode'),) + tuple(cached_result[5:])
|
||||
else:
|
||||
cached_result = tuple(cached_result[:4]) + (file_info.get('episode'),)
|
||||
# Ensure tuple is 6 elements (pad with "CURRENT" if needed)
|
||||
if len(cached_result) == 5:
|
||||
cached_result = cached_result + ("CURRENT",)
|
||||
result = self.update_episode_count(cached_result)
|
||||
|
||||
# If it's different, update in cache as well.
|
||||
if cached_result != result and result:
|
||||
print(f'Updating cache to: {result}')
|
||||
self.update_cache(filename, file_info.get('name'), result, line_index)
|
||||
|
||||
# If it either errored or couldn't update, retry without cache.
|
||||
if not result:
|
||||
print('Failed to update through cache, retrying without.')
|
||||
# Deleting from the cache
|
||||
self.update_cache(filename, file_info.get('name'), None, line_index)
|
||||
# Retrying
|
||||
self.handle_filename(filename)
|
||||
else:
|
||||
print('Cached result is invalid, ignoring cache.')
|
||||
# Remove invalid cache entry
|
||||
if line_index is not None:
|
||||
self.update_cache(filename, file_info.get('name'), None, line_index)
|
||||
# Retry without cache
|
||||
self.handle_filename(filename)
|
||||
|
||||
return
|
||||
|
||||
# Hardcoded exceptions to fix detection
|
||||
# Easier than just renaming my files 1 by 1 on Qbit
|
||||
# Every exception I find will be added here
|
||||
def fix_filename(self, path_parts):
|
||||
"""
|
||||
Applies hardcoded exceptions and fixes to the filename and folder structure for better title detection.
|
||||
Args:
|
||||
path_parts (list): List of path components.
|
||||
Returns:
|
||||
list: Modified path components.
|
||||
"""
|
||||
guess = guessit(path_parts[-1], self.OPTIONS) # Simply easier for fixing the filename if we have what it is detecting.
|
||||
|
||||
path_parts[-1] = os.path.splitext(path_parts[-1])[0]
|
||||
pattern = r'[\\\/:!\*\?"<>\|\._-]'
|
||||
|
||||
title_depth = -1
|
||||
|
||||
# Fix from folders if the everything is not in the filename
|
||||
if 'title' not in guess:
|
||||
# Depth=2
|
||||
for depth in range(2, min(4, len(path_parts))):
|
||||
folder_guess = guessit(path_parts[-depth], self.OPTIONS)
|
||||
if 'title' in folder_guess:
|
||||
guess['title'] = folder_guess['title']
|
||||
title_depth = -depth
|
||||
break
|
||||
|
||||
if 'title' not in guess:
|
||||
print(f"Couldn't find title in filename '{path_parts[-1]}'! Guess result: {guess}")
|
||||
return path_parts
|
||||
|
||||
# Only clean up titles for some series
|
||||
cleanup_titles = ['Ranma', 'Chi', 'Bleach', 'Link Click']
|
||||
if any(title in guess['title'] for title in cleanup_titles):
|
||||
path_parts[title_depth] = re.sub(pattern, ' ', path_parts[title_depth])
|
||||
path_parts[title_depth] = " ".join(path_parts[title_depth].split())
|
||||
|
||||
if 'Centimeters per Second' == guess['title'] and 5 == guess.get('episode', 0):
|
||||
path_parts[title_depth] = path_parts[title_depth].replace(' 5 ', ' Five ')
|
||||
# For some reason AniList has this film in 3 parts.
|
||||
path_parts[title_depth] = path_parts[title_depth].replace('per Second', 'per Second 3')
|
||||
|
||||
return path_parts
|
||||
|
||||
# Parse the file name using guessit
|
||||
def parse_filename(self, filepath):
|
||||
"""
|
||||
Parses the filename and folder structure to extract anime title, episode, season, and year.
|
||||
Args:
|
||||
filepath (str): The path to the video file.
|
||||
Returns:
|
||||
dict: Parsed info with keys 'name', 'episode', 'year'.
|
||||
"""
|
||||
path_parts = self.fix_filename(filepath.replace('\\', '/').split('/'))
|
||||
filename = path_parts[-1]
|
||||
name, season, part, year = '', '', '', ''
|
||||
episode = 1
|
||||
# First, try to guess from the filename
|
||||
guess = guessit(filename, self.OPTIONS)
|
||||
print(f'File name guess: {filename} -> {dict(guess)}')
|
||||
|
||||
# Episode guess from the title.
|
||||
# Usually, releases are formated [Release Group] Title - S01EX
|
||||
|
||||
# If the episode index is 0, that would mean that the episode is before the title in the filename
|
||||
# Which is a horrible way of formatting it, so assume its wrong
|
||||
|
||||
# If its 1, then the title is probably 0, so its okay. (Unless season is 0)
|
||||
# Really? What is the format "S1E1 - {title}"? That's almost psycopathic.
|
||||
|
||||
# If its >2, theres probably a Release Group and Title / Season / Part, so its good
|
||||
|
||||
episode = guess.get('episode', None)
|
||||
season = guess.get('season', '')
|
||||
part = str(guess.get('part', ''))
|
||||
year = str(guess.get('year', ''))
|
||||
|
||||
# Quick fixes assuming season before episode
|
||||
# 'episode_title': '02' in 'S2 02'
|
||||
if guess.get('episode_title', '').isdigit() and episode is None:
|
||||
print(f'Detected episode in episode_title. Episode: {int(guess.get("episode_title"))}')
|
||||
episode = int(guess.get('episode_title'))
|
||||
|
||||
# 'episode': [86, 13] (EIGHTY-SIX), [1, 2, 3] (RANMA) lol.
|
||||
if isinstance(episode, list):
|
||||
print(f'Detected multiple episodes: {episode}. Picking last one.')
|
||||
episode = episode[-1]
|
||||
|
||||
# 'season': [2, 3] in "S2 03"
|
||||
if isinstance(season, list):
|
||||
print(f'Detected multiple seasons: {season}. Picking first one as season.')
|
||||
if episode is None:
|
||||
print('Episode still not detected. Picking last position of the season list.')
|
||||
episode = season[-1]
|
||||
|
||||
season = season[0]
|
||||
|
||||
episode = episode or 1
|
||||
season = str(season)
|
||||
|
||||
keys = list(guess.keys())
|
||||
episode_index = keys.index('episode') if 'episode' in guess else 1
|
||||
season_index = keys.index('season') if 'season' in guess else -1
|
||||
title_in_filename = 'title' in guess and (episode_index > 0 and (season_index > 0 or season_index == -1))
|
||||
|
||||
# If the title is not in the filename or episode index is 0, try the folder name
|
||||
# If the episode index > 0 and season index > 0, its safe to assume that the title is in the file name
|
||||
|
||||
if title_in_filename:
|
||||
name = guess['title']
|
||||
else:
|
||||
# If it isnt in the name of the file, try to guess using the name of the folder it is stored in
|
||||
|
||||
# Depth=2 folders
|
||||
for depth in [2, 3]:
|
||||
folder_guess = guessit(path_parts[-depth], self.OPTIONS) if len(path_parts) > depth-1 else ''
|
||||
if folder_guess != '':
|
||||
print(f'{depth-1}{"st" if depth-1==1 else "nd"} Folder guess:\n{path_parts[-depth]} -> {dict(folder_guess)}')
|
||||
|
||||
name = str(folder_guess.get('title', ''))
|
||||
season = season or str(folder_guess.get('season', ''))
|
||||
part = part or str(folder_guess.get('part', ''))
|
||||
year = year or str(folder_guess.get('year', ''))
|
||||
|
||||
# If we got the name, its probable we already got season and part from the way folders are usually structured
|
||||
if name != '':
|
||||
break
|
||||
|
||||
# Add season and part if there are
|
||||
if season and (int(season) > 1 or part):
|
||||
name += f" Season {season}"
|
||||
|
||||
if part:
|
||||
name += f" Part {part}"
|
||||
|
||||
print('Guessed name: ' + name)
|
||||
return {
|
||||
'name': name,
|
||||
'episode': episode,
|
||||
'year': year,
|
||||
}
|
||||
|
||||
def get_anime_info_and_progress(self, name, file_progress, year):
|
||||
"""
|
||||
Queries AniList for anime info and user progress for a given title and year.
|
||||
Args:
|
||||
name (str): Anime title.
|
||||
file_progress (int): Episode number from the file.
|
||||
year (str): Year string (may be empty).
|
||||
Returns:
|
||||
tuple: (anime_id, anime_name, current_progress, total_episodes, file_progress, current_status)
|
||||
"""
|
||||
query = '''
|
||||
query($search: String, $year: FuzzyDateInt, $page: Int) {
|
||||
Page(page: $page) {
|
||||
media (search: $search, type: ANIME, startDate_greater: $year) {
|
||||
id
|
||||
title { romaji }
|
||||
season
|
||||
seasonYear
|
||||
episodes
|
||||
duration
|
||||
format
|
||||
status
|
||||
mediaListEntry {
|
||||
status
|
||||
progress
|
||||
media {
|
||||
episodes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
variables = {'search': name, 'year': year or 1, 'page': 1}
|
||||
|
||||
response = self.make_api_request(query, variables, self.access_token)
|
||||
if response and 'data' in response:
|
||||
seasons = response['data']['Page']['media']
|
||||
# This is the first element, which is the same as Media(search: $search)
|
||||
|
||||
if len(seasons) == 0:
|
||||
raise Exception(f"Couldn\'t find an anime from this title! ({name})")
|
||||
|
||||
entry = seasons[0]['mediaListEntry']
|
||||
anime_data = (
|
||||
seasons[0]['id'],
|
||||
seasons[0]['title']['romaji'],
|
||||
entry['progress'] if entry is not None else None,
|
||||
seasons[0]['episodes'],
|
||||
file_progress,
|
||||
entry['status'] if entry is not None else None
|
||||
)
|
||||
# If the episode in the file name is larger than the total amount of episodes
|
||||
# Then they are using absolute numbering format for episodes (looking at you SubsPlease)
|
||||
# Try to guess season and episode.
|
||||
if seasons[0]['episodes'] is not None and file_progress > seasons[0]['episodes']:
|
||||
seasons = self.filter_valid_seasons(seasons)
|
||||
print('Related shows:', ", ".join(season["title"]["romaji"] for season in seasons))
|
||||
|
||||
anime_data = self.find_season_and_episode(seasons, file_progress)
|
||||
|
||||
found_season = next((season for season in seasons if season['id'] == anime_data[0]), None)
|
||||
found_entry = found_season['mediaListEntry'] if found_season and found_season['mediaListEntry'] else None
|
||||
anime_data = (
|
||||
anime_data[0],
|
||||
anime_data[1],
|
||||
anime_data[2],
|
||||
anime_data[3],
|
||||
anime_data[4],
|
||||
found_entry['status'] if found_entry else None
|
||||
)
|
||||
print(f"Final guessed anime: {found_season}")
|
||||
print(f'Absolute episode {file_progress} corresponds to Anime: {anime_data[1]}, Episode: {anime_data[-2]}')
|
||||
else:
|
||||
print(f"Final guessed anime: {seasons[0]}")
|
||||
return anime_data
|
||||
return (None, None, None, None, None, None)
|
||||
|
||||
# Update the anime based on file progress
|
||||
def update_episode_count(self, result):
|
||||
"""
|
||||
Updates the episode count and/or status for an anime entry on AniList, according to user settings.
|
||||
Args:
|
||||
result (tuple): (anime_id, anime_name, current_progress, total_episodes, file_progress, current_status)
|
||||
Returns:
|
||||
tuple or bool: Updated result tuple, or False on failure.
|
||||
"""
|
||||
if result is None:
|
||||
raise Exception('Parameter in update_episode_count is null.')
|
||||
|
||||
anime_id, anime_name, current_progress, total_episodes, file_progress, current_status = result
|
||||
|
||||
# Only launch anilist
|
||||
if sys.argv[2] == 'launch':
|
||||
print(f'Opening AniList for "{anime_name}": https://anilist.co/anime/{anime_id}')
|
||||
webbrowser.open_new_tab(f'https://anilist.co/anime/{anime_id}')
|
||||
return result
|
||||
|
||||
if current_progress is None:
|
||||
raise Exception('Failed to get current episode count. Is it on your list?')
|
||||
|
||||
# Handle completed -> rewatching on first episode
|
||||
if (current_status == 'COMPLETED' and file_progress == 1 and self.options['SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE']):
|
||||
# Needs to update in 2 steps, since AniList doesn't allow setting progress while changing the status from completed to rewatching. If you try, it will just reset the progress to 0.
|
||||
print('Setting status to REPEATING (rewatching) and updating progress for first episode of completed anime.')
|
||||
# Step 1: Set to REPEATING, progress=0
|
||||
query = '''
|
||||
mutation ($mediaId: Int, $progress: Int, $status: MediaListStatus) {
|
||||
SaveMediaListEntry (mediaId: $mediaId, progress: $progress, status: $status) {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
}
|
||||
'''
|
||||
variables = {'mediaId': anime_id, 'progress': 0, 'status': 'REPEATING'}
|
||||
response = self.make_api_request(query, variables, self.access_token)
|
||||
# Step 2: Set progress to 1
|
||||
variables = {'mediaId': anime_id, 'progress': 1}
|
||||
response = self.make_api_request(query, variables, self.access_token)
|
||||
if response and 'data' in response:
|
||||
updated_progress = response['data']['SaveMediaListEntry']['progress']
|
||||
print(f'Episode count updated successfully! New progress: {updated_progress}')
|
||||
return (anime_id, anime_name, updated_progress, total_episodes, 1, 'REPEATING')
|
||||
print('Failed to update episode count.')
|
||||
return False
|
||||
# Handle updating progress for rewatching
|
||||
if (current_status == 'REPEATING' and self.options['UPDATE_PROGRESS_WHEN_REWATCHING']):
|
||||
print('Updating progress for anime set to REPEATING (rewatching).')
|
||||
status_to_set = 'REPEATING'
|
||||
# Only update if status is CURRENT or PLANNING
|
||||
elif current_status in ['CURRENT', 'PLANNING']:
|
||||
# If its lower than the current progress, dont update.
|
||||
if file_progress <= current_progress:
|
||||
raise Exception(f'Episode was not new. Not updating ({file_progress} <= {current_progress})')
|
||||
status_to_set = 'CURRENT' if file_progress != total_episodes else None
|
||||
else:
|
||||
raise Exception(f'Anime is not in a modifiable state (status: {current_status}). Not updating.')
|
||||
|
||||
set_to_completed = False
|
||||
if file_progress == total_episodes:
|
||||
if current_status == 'CURRENT' and self.options['SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT']:
|
||||
set_to_completed = True
|
||||
elif current_status == 'REPEATING' and self.options['SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING']:
|
||||
set_to_completed = True
|
||||
|
||||
query = '''
|
||||
mutation ($mediaId: Int, $progress: Int, $status: MediaListStatus) {
|
||||
SaveMediaListEntry (mediaId: $mediaId, progress: $progress, status: $status) {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
variables = {'mediaId': anime_id, 'progress': file_progress}
|
||||
if status_to_set:
|
||||
variables['status'] = status_to_set
|
||||
|
||||
response = self.make_api_request(query, variables, self.access_token)
|
||||
if response and 'data' in response:
|
||||
updated_progress = response['data']['SaveMediaListEntry']['progress']
|
||||
print(f'Episode count updated successfully! New progress: {updated_progress}')
|
||||
|
||||
if set_to_completed:
|
||||
print('Setting status to COMPLETED after last episode.')
|
||||
complete_query = '''
|
||||
mutation ($mediaId: Int, $status: MediaListStatus) {
|
||||
SaveMediaListEntry (mediaId: $mediaId, status: $status) {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
}
|
||||
'''
|
||||
complete_variables = {'mediaId': anime_id, 'status': 'COMPLETED'}
|
||||
complete_response = self.make_api_request(complete_query, complete_variables, self.access_token)
|
||||
if complete_response and 'data' in complete_response:
|
||||
print('Status set to COMPLETED successfully.')
|
||||
else:
|
||||
print('Failed to set status to COMPLETED.')
|
||||
|
||||
return (anime_id, anime_name, updated_progress, total_episodes, file_progress, current_status)
|
||||
print('Failed to update episode count.')
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for the script. Handles encoding and runs the updater.
|
||||
"""
|
||||
try:
|
||||
# Reconfigure to utf-8
|
||||
if sys.stdout.encoding != 'utf-8':
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
except Exception as e_reconfigure:
|
||||
print(f"Couldn\'t reconfigure stdout/stderr to UTF-8: {e_reconfigure}", file=sys.stderr)
|
||||
# Parse options from argv[3] if present
|
||||
options = {
|
||||
"SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE": False,
|
||||
"UPDATE_PROGRESS_WHEN_REWATCHING": True,
|
||||
"SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT": False,
|
||||
"SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING": True
|
||||
}
|
||||
if len(sys.argv) > 3:
|
||||
user_options = json.loads(sys.argv[3])
|
||||
options.update(user_options)
|
||||
|
||||
# Pass options to AniListUpdater
|
||||
updater = AniListUpdater(options)
|
||||
updater.handle_filename(sys.argv[1])
|
||||
|
||||
except Exception as e:
|
||||
print(f'ERROR: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
262
submodules/mpv-anilist-updater/anilistUpdater/main.lua
Normal file
262
submodules/mpv-anilist-updater/anilistUpdater/main.lua
Normal file
@@ -0,0 +1,262 @@
|
||||
--[[
|
||||
Configuration options for anilistUpdater (set in anilistUpdater.conf):
|
||||
|
||||
DIRECTORIES: Table or comma/semicolon-separated string. The directories the script will work on. Leaving it empty will make it work on every video you watch with mpv. Example: DIRECTORIES = {"D:/Torrents", "D:/Anime"}
|
||||
|
||||
UPDATE_PERCENTAGE: Number (0-100). The percentage of the video you need to watch before it updates AniList automatically. Default is 85 (usually before the ED of a usual episode duration).
|
||||
|
||||
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE: Boolean. If true, when watching episode 1 of a completed anime, set it to rewatching and update progress.
|
||||
|
||||
UPDATE_PROGRESS_WHEN_REWATCHING: Boolean. If true, allow updating progress for anime set to rewatching. This is for if you want to set anime to rewatching manually, but still update progress automatically.
|
||||
|
||||
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT: Boolean. If true, set to COMPLETED after last episode if status was CURRENT.
|
||||
|
||||
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING: Boolean. If true, set to COMPLETED after last episode if status was REPEATING (rewatching).
|
||||
]]
|
||||
local utils = require 'mp.utils'
|
||||
local mpoptions = require("mp.options")
|
||||
|
||||
local conf_name = "anilistUpdater.conf"
|
||||
local script_dir = (debug.getinfo(1).source:match("@?(.*/)") or "./")
|
||||
|
||||
-- Try script-opts directory (sibling to scripts)
|
||||
local script_opts_dir = script_dir:match("^(.-)[/\\]scripts[/\\]")
|
||||
|
||||
if script_opts_dir then
|
||||
script_opts_dir = utils.join_path(script_opts_dir, "script-opts")
|
||||
else
|
||||
-- Fallback: try to find mpv config dir
|
||||
script_opts_dir = os.getenv("APPDATA") and utils.join_path(utils.join_path(os.getenv("APPDATA"), "mpv"), "script-opts") or
|
||||
os.getenv("HOME") and utils.join_path(utils.join_path(utils.join_path(os.getenv("HOME"), ".config"), "mpv"), "script-opts") or
|
||||
nil
|
||||
end
|
||||
|
||||
local script_opts_path = script_opts_dir and utils.join_path(script_opts_dir, conf_name) or nil
|
||||
|
||||
-- Try script directory
|
||||
local script_path = utils.join_path(script_dir, conf_name)
|
||||
|
||||
-- Try mpv config directory
|
||||
local mpv_conf_dir = os.getenv("APPDATA") and utils.join_path(os.getenv("APPDATA"), "mpv") or os.getenv("HOME") and
|
||||
utils.join_path(utils.join_path(os.getenv("HOME"), ".config"), "mpv") or nil
|
||||
local mpv_conf_path = mpv_conf_dir and utils.join_path(mpv_conf_dir, conf_name) or nil
|
||||
|
||||
local conf_paths = {script_opts_path, script_path, mpv_conf_path}
|
||||
|
||||
local default_conf = [[
|
||||
# Use 'yes' or 'no' for boolean options below
|
||||
# Example for multiple directories (comma or semicolon separated):
|
||||
# DIRECTORIES=D:/Torrents,D:/Anime
|
||||
# or
|
||||
# DIRECTORIES=D:/Torrents;D:/Anime
|
||||
DIRECTORIES=
|
||||
UPDATE_PERCENTAGE=85
|
||||
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE=no
|
||||
UPDATE_PROGRESS_WHEN_REWATCHING=yes
|
||||
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT=yes
|
||||
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING=yes
|
||||
]]
|
||||
|
||||
-- Try to find config file
|
||||
local conf_path = nil
|
||||
for _, path in ipairs(conf_paths) do
|
||||
if path then
|
||||
local f = io.open(path, "r")
|
||||
if f then
|
||||
f:close()
|
||||
conf_path = path
|
||||
print("Found config at: " .. path)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If not found, try to create in order
|
||||
if not conf_path then
|
||||
for _, path in ipairs(conf_paths) do
|
||||
if path then
|
||||
local f = io.open(path, "w")
|
||||
if f then
|
||||
f:write(default_conf)
|
||||
f:close()
|
||||
conf_path = path
|
||||
print("Created config at: " .. path)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If still not found or created, warn and use defaults
|
||||
if not conf_path then
|
||||
mp.msg.warn("Could not find or create anilistUpdater.conf in any known location! Using default options.")
|
||||
end
|
||||
|
||||
-- Now load options as usual
|
||||
local options = {
|
||||
DIRECTORIES = "",
|
||||
UPDATE_PERCENTAGE = 85,
|
||||
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE = false,
|
||||
UPDATE_PROGRESS_WHEN_REWATCHING = true,
|
||||
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT = true,
|
||||
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING = true
|
||||
}
|
||||
if conf_path then
|
||||
mpoptions.read_options(options, "anilistUpdater")
|
||||
end
|
||||
|
||||
-- Parse DIRECTORIES if it's a string (comma or semicolon separated)
|
||||
if type(options.DIRECTORIES) == "string" and options.DIRECTORIES ~= "" then
|
||||
local dirs = {}
|
||||
for dir in string.gmatch(options.DIRECTORIES, "([^,;]+)") do
|
||||
table.insert(dirs, (dir:gsub("^%s*(.-)%s*$", "%1"))) -- trim
|
||||
end
|
||||
options.DIRECTORIES = dirs
|
||||
elseif type(options.DIRECTORIES) == "string" then
|
||||
options.DIRECTORIES = {}
|
||||
end
|
||||
|
||||
-- When calling Python, pass only the options relevant to it
|
||||
local python_options = {
|
||||
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE = options.SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE,
|
||||
UPDATE_PROGRESS_WHEN_REWATCHING = options.UPDATE_PROGRESS_WHEN_REWATCHING,
|
||||
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT = options.SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT,
|
||||
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING = options.SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING
|
||||
}
|
||||
local python_options_json = utils.format_json(python_options)
|
||||
|
||||
DIRECTORIES = options.DIRECTORIES
|
||||
UPDATE_PERCENTAGE = tonumber(options.UPDATE_PERCENTAGE) or 85
|
||||
|
||||
local function path_starts_with_any(path, directories)
|
||||
for _, dir in ipairs(directories) do
|
||||
if path:sub(1, #dir) == dir then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function callback(success, result, error)
|
||||
if result.status == 0 then
|
||||
mp.osd_message("Updated anime correctly.", 2)
|
||||
end
|
||||
end
|
||||
|
||||
local function get_python_command()
|
||||
local os_name = package.config:sub(1, 1)
|
||||
if os_name == '\\' then
|
||||
-- Windows
|
||||
return "python"
|
||||
else
|
||||
-- Linux
|
||||
return "python3"
|
||||
end
|
||||
end
|
||||
|
||||
local python_command = get_python_command()
|
||||
|
||||
-- Make sure it doesnt trigger twice in 1 video
|
||||
local triggered = false
|
||||
|
||||
-- Function to check if we've reached the user-defined percentage of the video
|
||||
function check_progress()
|
||||
if triggered then
|
||||
return
|
||||
end
|
||||
|
||||
local percent_pos = mp.get_property_number("percent-pos")
|
||||
|
||||
if percent_pos then
|
||||
if percent_pos >= UPDATE_PERCENTAGE then
|
||||
update_anilist("update")
|
||||
triggered = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Function to launch the .py script
|
||||
function update_anilist(action)
|
||||
if action == "launch" then
|
||||
mp.osd_message("Launching AniList", 2)
|
||||
end
|
||||
local script_dir = debug.getinfo(1).source:match("@?(.*/)")
|
||||
local directory = mp.get_property("working-directory")
|
||||
-- It seems like in Linux working-directory sometimes returns it without a "/" at the end
|
||||
directory = (directory:sub(-1) == '/' or directory:sub(-1) == '\\') and directory or directory .. '/'
|
||||
-- For some reason, "path" sometimes returns the absolute path, sometimes it doesn't.
|
||||
local file_path = mp.get_property("path")
|
||||
local path = utils.join_path(directory, file_path)
|
||||
|
||||
local table = {}
|
||||
table.name = "subprocess"
|
||||
table.args = {python_command, script_dir .. "anilistUpdater.py", path, action, python_options_json}
|
||||
local cmd = mp.command_native_async(table, callback)
|
||||
end
|
||||
|
||||
mp.observe_property("percent-pos", "number", check_progress)
|
||||
|
||||
-- Reset triggered
|
||||
mp.register_event("file-loaded", function()
|
||||
triggered = false
|
||||
if #DIRECTORIES > 0 then
|
||||
local directory = mp.get_property("working-directory")
|
||||
directory = (directory:sub(-1) == '/' or directory:sub(-1) == '\\') and directory or directory .. '/'
|
||||
local file_path = mp.get_property("path")
|
||||
local path = utils.join_path(directory, file_path)
|
||||
path = path:gsub("\\", "/")
|
||||
|
||||
if not path_starts_with_any(path, DIRECTORIES) then
|
||||
mp.unobserve_property(check_progress)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Keybinds, modify as you please
|
||||
mp.add_key_binding('ctrl+a', 'update_anilist', function()
|
||||
update_anilist("update")
|
||||
end)
|
||||
|
||||
mp.add_key_binding('ctrl+b', 'launch_anilist', function()
|
||||
update_anilist("launch")
|
||||
end)
|
||||
|
||||
-- Open the folder that the video is
|
||||
function open_folder()
|
||||
local path = mp.get_property("path")
|
||||
local directory
|
||||
|
||||
if not path then
|
||||
mp.msg.warn("No file is currently playing.")
|
||||
return
|
||||
end
|
||||
|
||||
if path:find('\\') then
|
||||
directory = path:match("(.*)\\")
|
||||
elseif path:find('\\\\') then
|
||||
directory = path:match("(.*)\\\\")
|
||||
else
|
||||
directory = mp.get_property("working-directory")
|
||||
end
|
||||
|
||||
-- Use the system command to open the folder in File Explorer
|
||||
local args
|
||||
if package.config:sub(1, 1) == '\\' then
|
||||
-- Windows
|
||||
args = {'explorer', directory}
|
||||
elseif os.getenv("XDG_CURRENT_DESKTOP") or os.getenv("WAYLAND_DISPLAY") or os.getenv("DISPLAY") then
|
||||
-- Linux (assume a desktop environment like GNOME, KDE, etc.)
|
||||
args = {'xdg-open', directory}
|
||||
elseif package.config:sub(1, 1) == '/' then
|
||||
-- macOS
|
||||
args = {'open', directory}
|
||||
end
|
||||
|
||||
mp.command_native({
|
||||
name = "subprocess",
|
||||
args = args,
|
||||
detach = true
|
||||
})
|
||||
end
|
||||
|
||||
mp.add_key_binding('ctrl+d', 'open_folder', open_folder)
|
||||
Reference in New Issue
Block a user