Skip to content

wax911/anime-scrobbler

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

74 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🐧 🐍 Anime Scrobbler (for research purposes of course) 🐍 🐧

Just an automation utility/hobby project for fun, potentially useless? to download missing shows from your AniList collection not present in a plex media server.

How does it work?

This little app consists of 5 modules:

  • anilist

    Handles requests to anilist to fetch your list/s

  • app

    Central point of the application, handles calls to other modules, database operations, utilities, e.t.c.

  • nyaa

    Queries nyaa.si for torrents matching shows missing in your plex media server but available in one or more of your lists and any shows in a given list name at run time. (see How do I use it?)

  • plex

    Handles searching and matching anilist meida agains plex library shows. This module will use romaji, english and synonyms from anilist when searching in plex until a match is found, and each result is tested for a 98% match (this avoids false negatives given shows in plex may slightly differ from anilist names).

  • transmission

    Handles dispatching torrents to Transmission for downloading

Q: What versin of python is required?
A: Python 3.7+ because the application uses @dataclass annotations

Q: What about python 2?
A: πŸ˜† that's funny

Q: Where are the unit tests?
A: I didn't have time πŸ’©

Q: What if I don't have transmission?
A: The torrent files will be downloaded in a local directory labled torrents ./app/torrents

Q: Are there any limitations?
A: Yes!! ofcourse 😈 πŸ›

  • Sometimes plexapi returns 0 results for a search query, desipite the search term being 100% exact (not sure why this happens) and because of this the application will assume this item doesn't exist in your plex library and add it to the list of missing shows
  • Due to how Plex presents it's shows as seasons and AniList represents media as individual items and nyaa has a lot of variation, I can't guarantee that shows will be found or matched correctly.
  • More??? You tell me_

How do I use it?

Before we get started I will assume that you have some basic knowledge regarding python and pip Tip: Google is your friend πŸ˜‰

  • Create your configuration files in .app/config/
  • Create your authentication files in .app/auth/
  • Install Python 3.7 and Python virtual environment and set it up
  • Install the dependencies pip install -r requirements.txt

You can find configuration instructions here and authentication instructions here

The application is CLI based and you can expose the available commands through:

python manage.py --help

N.B if you're not running python

Dependencies

keyword | String | Keyword for the search query category | Integer | See Categories subcategory | Integer | See Categories filters | Integer | See Filters page | Integer | Number between 0 and 1000

Nyaa.search(keyword="Shoukoku no Altair", category=1)

Returns a list of dictionaries like this

{
   "category": "Anime - English-translated",
   "url": "https://nyaa.si/view/968600",
   "name": "[HorribleSubs] Shoukoku no Altair - 14 [720p].mkv",
   "download_url": "https://nyaa.si/download/968600.torrent",
   "magnet": "<magnet torrent URI>",
   "size": "317.2 MiB",
   "date": "2017-10-13 20:16",
   "seeders": "538",
   "leechers": "286",
   "completed_downloads": "852"
}

TinyDB is a lightweight document oriented database optimized for your happiness :) It's written in pure Python and has no external dependencies. The target are small apps that would be blown away by a SQL-DB or an external database server.

Example Code

from tinydb import TinyDB
db = TinyDB('/path/to/db.json')
db.insert({'int': 1, 'char': 'a'})
db.insert({'int': 1, 'char': 'b'})

Query Language

from tinydb import TinyDB, Query

User = Query()
db = TinyDB('/path/to/db.json')
# Search for a field value
db.search(User.name == 'John')
[{'name': 'John', 'age': 22}, {'name': 'John', 'age': 37}]

# Combine two queries with logical and
db.search((User.name == 'John') & (User.age <= 30))
[{'name': 'John', 'age': 22}]

# Combine two queries with logical or
db.search((User.name == 'John') | (User.name == 'Bob'))
[{'name': 'John', 'age': 22}, {'name': 'John', 'age': 37}, {'name': 'Bob', 'age': 42}]

# More possible comparisons:  !=  <  >  <=  >=
# More possible checks: where(...).matches(regex), where(...).test(your_test_func)

Tables

table = db.table('name')
table.insert({'value': True})
table.all()
[{'value': True}]

There are two types of authentication. If you are running on a separate network or using Plex Users you can log into MyPlex to get a PlexServer instance. An example of this is below. NOTE: Servername below is the name of the server (not the hostname and port). If logged into Plex Web you can see the server name in the top left above your available libraries.

from plexapi.myplex import MyPlexAccount

account = MyPlexAccount('<USERNAME>', '<PASSWORD>')  
# returns a PlexServer instance
plex = account.resource('<SERVERNAME>').connect()

If you want to avoid logging into MyPlex and you already know your auth token string, you can use the PlexServer object directly as above, but passing in the baseurl and auth token directly.

from plexapi.server import PlexServer

baseurl = 'http://plexserver:32400'
token = '2ffLuB84dqLswk9skLos'
plex = PlexServer(baseurl, token)

Usage examples

The example below shows how you can execute queries against a local schema.

from gql import gql, Client

client = Client(schema=schema)
query = gql('''
{
  hello
}
''')

client.execute(query)

This module simplifies creation of data classes ([PEP 557][pep-557]) from dictionaries.

from dataclasses import dataclass
from dacite import from_dict


@dataclass
class User:
    name: str
    age: int
    is_active: bool


data = {
    'name': 'john',
    'age': 30,
    'is_active': True,
}

user = from_dict(data_class=User, data=data)

assert user == User(name='john', age=30, is_active=True)

Anitopy is a Python library for parsing anime video filenames. It's simple to use and it's based on the C++ library Anitomy <https://github.com/erengy/anitomy>_.

import anitopy
anitopy.parse('[TaigaSubs]_Toradora!_(2008)_-_01v2_-_Tiger_and_Dragon_[1280x720_H.264_FLAC][1234ABCD].mkv')

Will result in the following:

{
   "anime_title": "Toradora!",
   "anime_year": "2008",
   "audio_term": "FLAC",
   "episode_number": "01",
   "episode_title": "Tiger and Dragon",
   "file_checksum": "1234ABCD",
   "file_extension": "mkv",
   "file_name": "[TaigaSubs]_Toradora!_(2008)_-_01v2_-_Tiger_and_Dragon_[1280x720_H.264_FLAC][1234ABCD].mkv",
   "release_group": "TaigaSubs",
   "release_version": "2",
   "video_resolution": "1280x720",
   "video_term": "H.264"
}

clutch was designed to be a more lightweight and consistent Transmission RPC library than what was currently available for Python. Instead of simply using the keys/fields in the Transmission RPC spec which have a mix of dashed separated words and mixed case words, clutch tries to convert all keys to be more Pythonic: underscore separated words. This conversion is done so that it is still possible to specify the fields/argument specified in the Transmission RPC spec, but if you do so your mileage may vary (probably want to avoid it).

To install:

pip install transmission-clutch

To use:

>>> from clutch.core import Client

The module exports a function that takes an Unicode object (Python 2.x) or string (Python 3.x) and returns a string (that can be encoded to ASCII bytes in Python 3.x):

from unidecode import unidecode
unidecode(u'ko\u017eu\u0161\u010dek')
# outputs > 'kozuscek'
unidecode(u'30 \U0001d5c4\U0001d5c6/\U0001d5c1')
# outputs > '30 km/h'
unidecode(u"\u5317\u4EB0")
# outputs > 'Bei Jing '