Object Oriented UDP Client for AniDB, originally forked from adba.
I created this mainly to be able to add new files to my "mylist" on anidb when I add them to my local collection. As I tend to rip my own files and have no intention of spreading them to a wide audience, I needed to add these as "generic" files to anidb. And manual work is always less fun then automating said work...
As the anidb UDP API enforces a very slow rate of requests, this implementation caches all information requested from anidb and uses the cache whenever possible. The cache is stored in mysql (or any other sqlalchemy-compatible database). For how long does it cache? It depends! Shortest caching period is one day, after that some not very inteligent algorithm will add some probability score which is used to decide if the cache should be updated or not. It's untuned and will probably be difficult to get right for all use cases... I'm listening to any ideas about how to make this better.
Also, you can always force an update of the cache by using the objects update() method.
The Anime title search is implemented using the animetitles.xml file hosted at anidb. It is automatically downloaded and stored localy (for now hardcoded to /var/tmp/adbb/animetitles.xml.gz). This animetitles file is also cached for 7 days (using mtime to calculate age) and then is automatically updated. You can of course "update" it manually by removing the cached file.
Since version 1 adbb also supports tvdb/tmdb/imdb-mapping via Anime-Lists.
- recent python
- pycryptodome (required hash methods has been removed from the standard python library)
- sqlalchemy
- sqlalchemy-compatible database:
- mysql
- sqlite
- postgresql (the one most recently used and tested)
import adbb
user="<anidb-username>"
pwd="<anidb-password>"
sql="sqlite:///adbb.db"
# initialize backend
adbb.init(user, pwd, sql, debug=True)
# anime object can be created either with a name or anime ID
anime = adbb.Anime("Kemono no Souja Erin")
#anime = adbb.Anime(6187)
# this will print "Kemono no Souja Erin has 50 episodes and is a TV Series"
print("{} has {} episodes and is a {}".format(anime.title, anime.nr_of_episodes, anime.type))
# Episode object can be created either using anime+episode number or the anidb eid
# anime can be either aid, title or Anime object
episode = adbb.Episode(anime=anime, epno=5)
#episode = adbb.Episode(eid=96461)
# this will print "'Kemono no Souja Erin' episode 5 has title 'Erin and the Egg Thieves'"
print("'{}' episode {} has title '{}'".format(episode.anime.title, episode.episode_number, episode.title_eng))
# file can either be created with a local file, an anidb file id (fid) or
# using Anime and Episode
file = File(path="/media/Anime/Series/Kemono no Souja Erin/[winterbird] Kemono no Souja Erin - 05 [8EEAA040].mkv")
#file = File(fid=<some-fid>)
#file = File(anime=anime, episode=episode)
# note that most of the time this will work even if we use a file that is not in the anidb database
# will print "'<path>' contains episode 5 of 'Kemono no Souja Erin'. Mylist state is 'on hdd'"
print("'{}' contains episode {} of '{}'. Mylist state is '{}'".format(file.path, file.episode.episode_number, file.anime.title, file.mylist_state))
# adbb supports fetching posters. download_image() supports Anime and Group objects
# (afaik, there are no other images to get from anidb)
# For other images, check the fanart section below.
with open('poster.jpg', 'wb') as f:
adbb.download_image(f, anime)
# To log out from UDP api, make sure to run adbb.close() before exit
adbb.close()
Anime(init)
'init' can be either a title or aid. Titles are searched in the animetitles.xml file using fuzzy text matching (implemented using difflib). Only a single Anime is created, using the best title match. Note that some titles are ambigious. A search for 'Ranma', for example, can return either the series 'Ranma 1/2' (which has "Ranma" as a synonym) or 'Ranma 1/2 Nettou Hen' which has "Ranma" as an official title).
aid
- AniDB anime IDtitles
- A list of all titles for this Animetitle
- main title of this Animeupdated
- datetime when information about this Anime was last fetched from AniDBtvdbid
- TVDB ID for this anime or None if not available.tmdbid
- TMDB ID for this anime or None if not available. Can be a list if this Anime maps to multiple movies, use the tmdbid attribute on the Episode object to get the tmdbid for a specific episode.imdbid
- IMDB ID for this anime or None if not available. Can be a list if thie Anime maps to multiple movies, use the imdbid attribute on the Episode object to get the imdbid for a specific episode.relations
- A list of tuples containing relations to this anime. The first entry in the tuple is a string describing the relation type and the second is an Anime-object for the related anime.fanart
- if enabled it will return a list of dicts directly translated from the json returned from the fanart.tv API. Returns an empty list if not enabled.
The following attributes as returned from the AniDB API
year
type
nr_of_episodes
highest_episode_number
special_ep_count
air_date
end_date
url
picname
rating
vote_count
temp_rating
temp_vote_count
average_review_rating
review_count
is_18_restricted
ann_id
allcinema_id
animenfo_id
anidb_updated
special_count
credit_count
other_count
trailer_count
parody_count
Episode(anime=None, epno=None, eid=None)
Episode object can be created by specifying both anime and epno, or using just eid. anime can be either a title, aid or an Anime object. epno should be either a string or int representing the episode number. eid should be an int.
eid
- AniDB episode IDanime
- Anime object for the anime series that this episode belongs toepisode_number
- The episode number (note that this is a string)updated
- datetime when information about this episode was last fetched from AniDBtvdb_episode
- A tuple containing(season, episode)
if this episode can be mapped to a TVDB episode. Note thatepisode
is usualy a string containing the episode number, but can also be a tuple with (episode_number, partnumber) or a string containing episode numbers separated by '+' if the anidb episode is mapped to part of a TVDB episode or vice versa.tmdbid
- TMDB ID for this episode or None if not available.imdbid
- IMDB ID for this episode or None if not available.
The following attributes as returned from the AniDB API
length
rating
votes
title_eng
title_romaji
title_kanji
aired
type
File(path=None, fid=None, anime=None, episode=None)
File object requires either path, fid, or anime and episode to be set. When setting anime and episode this file will either be a generic file, or the file you have in your mylist for this anime and episode. fid is obviously the AniDB file ID. Path is the fun one.
When a path is specified the library will first check the size and ed2k-hash to the AniDB database. If the file exists there this will obviously represent that file. If the file doesn't exist in the AniDB databse the library will try to figure out which anime and episode this file represents. The episode number is guessed from the filename by using some regex. If no episode number is found, adbb will check if the Anime only has a single episode; and if that is the case it will assume that the file has episode number '1'. The Anime title is guessed from the parent directory if there is a good-enough match in the animetitles- file, otherwise it's guessed from the filename. For details, check _guess_anime_ep_from_file() and _guess_epno_from_filename() in the File class in animeobjs.py, and get_titles() in anames.py.
The File object has some functions for managing the file in mylist.
update_mylist(state=None, watched=None, source=None, other=None)
remove_from_mylist()
The update_mylist() function can be used both to add and to edit a file in mylist. state can be one of 'unknown', 'on hdd', 'on cd' or 'deleted'. watched can be either True, False or an datetime object describing when it was watched.
anime
- Anime object for which anime this file containsepisode
- Episode object for which episode this file containsgroup
- Group object for file authorsmultiep
- List of episode numbers this file contains. The episode number parsing supports multiple episodes, but fetching from AniDB does not so this is not reliable and I'm not really sure what to do with it...fid
- File ID from AniDBpath
- Full Path to this file (if created with a path)size
- file size in bytesed2khash
- ed2k-hash, because AniDB still uses it...updated
- datetime when information about this file was last fetched from AniDB
The following attributes as returned from the AniDB API
lid
gid
is_deprecated
is_generic
crc_ok
file_version
censored
length_in_seconds
description
aired_date
mylist_state
mylist_filestate
mylist_viewed
mylist_viewdate
mylist_storage
mylist_source
mylist_other
Group(name=None, gid=None)
Group object requires either a name (can be either short or long name) or gid. A group created with a name is always considered valid, and will be saved to the database even if the name does not represent a group in AniDB. In that case both the name and the short name will be set to the given name, and all other atributes will be empty.
updated
- datetime when information about this file was last fetched from AniDB
The following attributes as returned from the AniDB API
gid
rating
votes
acount
fcount
name
short
irc_channel
irc_server
url
picname
founded
disbanded
dateflag
last_release
last_activity
The Anime object contains an attribute called fanart
that can be used to fetch available fanart for that series/movie from fanart.tv if two conditions are met:
- you must provide an API key either in the
init()
-call using the keywordfanart_api_key
or by providing it in an .netrc-file. - The series/movie must be properly mapped to a tvdb/tmdb/imdb-ID in Anime-Lists
The fanart
attribute just returns a list of metadata from the fanart.tv api; but the adbb.download_fanart()
-method can be used to download the actual fanart. This example downloads the first background fanart the api returned.
The attribute is directly translated from the json API, so for structure description you should check the fanart.tv API reference. Note that it differs slightly between series and movies.
import adbb
api_key='secret'
adbb.init('sqlite:///.adbb.db', netrc_file='.netrc', fanart_api_key=api_key)
anime = adbb.Anime("Kemono no Souja Erin")
fanart = anime.fanart
background_url = fanart[0]["showbackground"][0]["url"]
with open("background.jpg", "wb") as f:
# The "preview" keyword-argument is False by default, but can be
# set to "true" to download a low-resolution preview image
adbb.download_fanart(f, background_url, preview=False)
adbb.close()
Although you can provide usernames and passwords directly to the init()
call it can be useful to have them stored elsewhere. init()
supports the netrc_file
keyword argument to fetch authentication information from a .netrc-file. The library checks the .netrc
-file for the following credentials:
- anidb username, password and encryption key. The
account
option is used to set the encryption key (machinename must be one of 'api.anidb.net', 'api.anidb.info', 'anidb.net') - database credentials (machinename must match your mysql/postgres hostname)
- fanart API key (machinename must be one of 'fanart.tv', 'assets.fanart.tv', 'webservice.fanart.tv', 'api.fanart.tv'
machine api.anidb.net
username winterbird
password supersecretpassword
account supersecretencryptionkey
machine sql.localdomain
username adbb
password supersecretpassword
machine fanart.tv
account supersecretapikey
As per the UDP API specification encrypted network traffic is not enabled by default but must be manually activated by the user.
In the case of adbb, you activate encryption by providing your encryption key when initializeing the library. Either with the api_key
-keyword argument to init()
or by using a .netrc.
You specify the encryption key yourself in your AniDB Profile. It's way past 1990, you really shouldn't send usernames and password unencrypted over the internet.
The library contains two command line utilities for mylist management. These are purely implemented after personal need, but is probably useful for other people as well. The source code could also be consulted for inspiration to make other tools. For usage, run the command with --help.
Tool to manage the cache database. At the moment it can only clean the database of unwanted/uneeded stuff, but perhaps importing data to the cache could be supported at some point..
run adbb_cache --help
and adbb_cache <subcommand> --help
for usage. The most useful subcommands ar probably old
to remove stuff that hasn't been touched in a while (90 days by default), and file
which can be used to remove files from the database as well as (with the proper flags) from filesystem and mylist.
This tool does not use the UDP API, except if it's asked to remove files from mylist.
Tool to identify episode files and move/rename them for easy identification by for media centers. You should probably run it with --dry-run first to make sure it behaves as expected.
Glueware for AniDB<->jellyfin integration. Requires jellyfin-apiclient-python. For more information, and usage, for this tool, see JELLYFIN.md.
I'll do my best to keep the API stable, so if you just use the Objects the code should continue to work with new releases.
You should recreate the databse after every release. I haven't figure out how to make sane database migrations on schema changes, so for now you should repopulate the cache when upgrading (just remove the sqlite databasefile or drop and recreate the postgres/mysql database).
I'll be restrictive about behavioural changes, and try to document them when they occur, but no promises as of now.
In no particular order:
- importing cache from anidb mylist exports.
- add support for descriptions (The only(?) feature missing to create a full-featured media-center scraper). Unfortunately, episode-descriptions are not supported by the UDP API.
- any other feature request?