Skip to content

Commit

Permalink
Merge pull request #12 from SageTendo/dev
Browse files Browse the repository at this point in the history
v1.1.1
  • Loading branch information
SageTendo committed Jan 28, 2024
2 parents e0b4de0 + b2d1591 commit 343e963
Show file tree
Hide file tree
Showing 16 changed files with 301 additions and 223 deletions.
44 changes: 1 addition & 43 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ With this addon, you can easily keep track of your watched anime, plan to watch,
Stremio.

<b><font color="#FFFF00">
NB: Currently, this addon only handles catalogs. To get details and streams on a catalog item (anime), Anime Kitsu
and Torrentio addons addon must be installed alongside this addon.
NB: Currently, this addon only handles catalogs and meta content. Syncing watched content with MyAnimeList will be added
in the near future.
</b></font>

## Installation
Expand All @@ -23,6 +23,15 @@ To install the MyAnimeList Anime List addon, please follow these steps:
generated on the website to automatically add the addon to Stremio.
6. The MyAnimeList Anime List addon will be added to your Stremio and ready for use.

<h4 style="color: #FFFF00">
NB: If no content is being displayed, it could be the result of your session expiring.
to renew your session, return to the
<a href="https://mal-stremio.vercel.app/">Addon's website</a> to "Sign in with MyAnimelist" or
"Refresh MyAnimeList Session".
Afterwards, content should then show up in Stremio.
</h4>


## Usage

Once you have installed the MyAnimeList Anime List addon, you can access it within the Stremio media center. The addon
Expand Down
18 changes: 17 additions & 1 deletion app/api/mal.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def get_auth(self):
f'&redirect_uri={self.redirect_uri}'
return f'{AUTH_URL}/oauth2/authorize?{query_params}'

def get_token(self, authorization_code: str):
def get_access_token(self, authorization_code: str):
"""
Get the access token for MyAnimeList
:param authorization_code: Authorization Code from MyAnimeList
Expand All @@ -67,6 +67,22 @@ def get_token(self, authorization_code: str):
'refresh_token': resp_json['refresh_token']
}

def refresh_token(self, refresh_token: str):
"""
Refresh the access token for MyAnimeList
:param refresh_token: Refresh Token
:return: Access Token
"""
data = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'grant_type': 'refresh_token',
'refresh_token': refresh_token
}
resp = requests.post(f'{AUTH_URL}/oauth2/token', data=data)
resp.raise_for_status()
return resp.json()

@staticmethod
def get_user_details(token: str):
"""
Expand Down
16 changes: 15 additions & 1 deletion app/db/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,19 @@

client = MongoClient(Config.MONGO_URI)
db = client.get_database(Config.MONGO_DB)
anime_map_collection = db.get_collection(Config.MONGO_ANIME_MAP)
UID_map_collection = db.get_collection(Config.MONGO_UID_MAP)


def store_user(user_details):
user_id = user_details['id']
access_token = user_details['access_token']
refresh_tkn = user_details['refresh_token']
expires_in = user_details['expires_in']

user = UID_map_collection.find_one({'uid': user_id})
if user:
UID_map_collection.update_one(user, {'$set': {'access_token': access_token, 'refresh_token': refresh_tkn,
'expires_in': expires_in}})
else:
UID_map_collection.insert_one(
{'uid': user_id, 'access_token': access_token, 'refresh_token': refresh_tkn, 'expires_in': expires_in})
2 changes: 1 addition & 1 deletion app/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from app.api.mal import MyAnimeListAPI

mal_client = MyAnimeListAPI()
MAL_ID_PREFIX = "mal:"
MAL_ID_PREFIX = "mal"
IMDB_ID_PREFIX = "tt"
86 changes: 67 additions & 19 deletions app/routes/auth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from flask import Blueprint, request, render_template, session
import requests
from flask import Blueprint, request, url_for, session, flash
from werkzeug.utils import redirect

from app.db.db import UID_map_collection
from app.db.db import store_user
from app.routes import mal_client
from config import Config
from app.routes.utils import handle_error

auth_blueprint = Blueprint('auth', __name__)

Expand All @@ -14,30 +15,77 @@ def authorize_user():
Authorizes a user to access MyAnimeList's API
:return: redirects to MyAnimeList's auth page
"""
if 'user' in session:
flash("You are already logged in.", "warning")
return redirect(url_for('index'))
return redirect(mal_client.get_auth())


@auth_blueprint.route('/callback')
def redirect_url():
def callback():
"""
Callback URL from MyAnimeList
:return: A webpage with the manifest URL and Magnet URL
"""
code = request.args.get('code')
auth_data = mal_client.get_token(code)
access_token = auth_data['access_token']
# check if error occurred from MyAnimeList
if request.args.get('error'):
flash(request.args.get('error_description'), "danger")
return redirect(url_for('index'))

# Get the user's username
user_details = mal_client.get_user_details(access_token)
user_id = str(user_details['id'])
if 'user' in session:
flash("You are already logged in.", "warning")
return redirect(url_for('index'))

# Add the user to the database
user = UID_map_collection.find_one({'uid': user_id})
if user:
UID_map_collection.update_one(user, {'$set': {'access_token': access_token}})
else:
UID_map_collection.insert_one({'uid': user_id, 'access_token': access_token})
try:
# exchange auth code for access token
if not (auth_code := request.args.get('code', None)):
return redirect(url_for('index'))
resp = mal_client.get_access_token(auth_code)

manifest_url = f'{Config.PROTOCOL}://{Config.REDIRECT_URL}/{user_id}/manifest.json'
manifest_magnet = f'stremio://{Config.REDIRECT_URL}/{user_id}/manifest.json'
return render_template('index.html', manifest_url=manifest_url, manifest_magnet=manifest_magnet)
# get user details and append the access and refresh token the info
user_details = mal_client.get_user_details(token=resp['access_token'])
user_details['id'] = str(user_details['id'])
user_details['access_token'] = resp['access_token']
user_details['refresh_token'] = resp['refresh_token']
user_details['expires_in'] = resp['expires_in']

store_user(user_details)
session['user'] = user_details
flash("You are now logged in.", "success")
return redirect(url_for('index'))
except requests.HTTPError as e:
return handle_error(e)


@auth_blueprint.route('/refresh')
def refresh_token():
"""
Refreshes the access token for MyAnimeList
:return: redirects to the index page of the app
"""
if not (user_details := session.get('user', None)):
flash("Session expired! Please log in to MyAnimeList again.", "danger")
return redirect(url_for('index'))

try:
resp = mal_client.refresh_token(refresh_token=user_details['refresh_token'])
user_details['access_token'] = resp['access_token']
user_details['refresh_token'] = resp['refresh_token']
user_details['expires_in'] = resp['expires_in']

store_user(user_details)
session['user'] = user_details
flash("MyAnimeList session refreshed.", "success")
return redirect(url_for('index'))
except requests.HTTPError as e:
return handle_error(e)


@auth_blueprint.route('/logout')
def logout():
"""
Logs the user out and clears the session
:return: redirects to the index page of the app
"""
session.pop('user')
return redirect(url_for('index'))
74 changes: 71 additions & 3 deletions app/routes/catalog.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
from flask import Blueprint
import random

from flask import Blueprint, abort
from werkzeug.exceptions import abort

from . import mal_client
from . import mal_client, MAL_ID_PREFIX
from .manifest import MANIFEST
from .utils import respond_with, mal_to_meta, get_token
from .utils import respond_with
from ..db.db import UID_map_collection

catalog_bp = Blueprint('catalog', __name__)


def get_token(user_id: str):
if not (user := UID_map_collection.find_one({'uid': user_id})):
return abort(404, 'User not found')
return user['access_token']


@catalog_bp.route('/<user_id>/catalog/<catalog_type>/<catalog_id>.json')
@catalog_bp.route('/<user_id>/catalog/<catalog_type>/<catalog_id>/skip=<offset>.json')
def addon_catalog(user_id: str, catalog_type: str, catalog_id: str, offset: str = None):
Expand Down Expand Up @@ -85,3 +94,62 @@ def search_metas(user_id: str, catalog_type: str, catalog_id: str, search_query:
meta = mal_to_meta(anime_item)
meta_previews.append(meta)
return respond_with({'metas': meta_previews})


def mal_to_meta(anime_item: dict):
"""
Convert MAL anime item to a valid Stremio meta format
:param anime_item: The MAL anime item to convert
:return: Stremio meta format
"""
# Metadata stuff
formatted_content_id = None
if content_id := anime_item.get('id', None):
formatted_content_id = f"{MAL_ID_PREFIX}_{content_id}"

title = anime_item.get('title', None)
mean_score = anime_item.get('mean', None)
synopsis = anime_item.get('synopsis', None)

poster = None
if poster_objects := anime_item.get('main_picture', {}):
if poster := poster_objects.get('large', None):
poster = poster_objects.get('medium')

if genres := anime_item.get('genres', {}):
genres = [genre['name'] for genre in genres]

# Check for release info and format it if it exists
if start_date := anime_item.get('start_date', None):
start_date = start_date[:4] # Get the year only
start_date += '-'

if end_date := anime_item.get('end_date', None):
start_date += end_date

# Check for background key in anime_item
background = None
picture_objects = anime_item.get('pictures', [])
if len(picture_objects) > 0:
random_background_index = random.randint(0, len(picture_objects) - 1)
if background := picture_objects[random_background_index].get('large', None) is None:
background = picture_objects[random_background_index]['medium']

# Check for media type and filter out non series and movie types
if media_type := anime_item.get('media_type', None):
if media_type in ['ona', 'ova', 'special', 'tv', 'unknown']:
media_type = 'series'
elif media_type != 'movie':
media_type = None

return {
'id': formatted_content_id,
'name': title,
'type': media_type,
'genres': genres,
'poster': poster,
'background': background,
'imdbRating': mean_score,
'releaseInfo': start_date,
'description': synopsis
}
8 changes: 4 additions & 4 deletions app/routes/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

MANIFEST = {
'id': 'com.sagetendo.mal-stremio-addon',
'version': '0.1.0',
'version': '1.1.1',
'name': 'MAL-Stremio Addon',
'description': 'MyAnimeList watchlist addon '
'(Requires Anime Kitsu and Torrentio to be installed if you want to watch content)',
'description': 'Provides users with watchlist content from MyAnimeList within Stremio. '
'This addon only provides catalogs, with the help of AnimeKitsu',
'types': ['anime', 'series', 'movie'],

'catalogs': [
Expand All @@ -30,7 +30,7 @@
}
],

'resources': ['catalog'],
'resources': ['catalog', 'meta'],
'idPrefixes': [MAL_ID_PREFIX, IMDB_ID_PREFIX]
}

Expand Down
Loading

0 comments on commit 343e963

Please sign in to comment.