612 lines
22 KiB
Python
612 lines
22 KiB
Python
"""AnimeEasySync module"""
|
|
from hashlib import sha256
|
|
from random import choice
|
|
from string import ascii_lowercase, digits
|
|
from json import dumps, JSONEncoder, JSONDecoder
|
|
from base64 import b64decode, b64encode
|
|
from requests import Request
|
|
from requests.auth import HTTPBasicAuth
|
|
from flask import (
|
|
Blueprint, render_template, request, session, redirect, url_for, current_app
|
|
)
|
|
from flask_wtf import FlaskForm
|
|
from wtforms import (
|
|
StringField, PasswordField, SubmitField, HiddenField, SelectField
|
|
)
|
|
from wtforms.validators import DataRequired
|
|
from .model import (
|
|
read_mal, read_anilist, read_kitsu, get_kitsu_user_id, UPDATE_FUNCS
|
|
)
|
|
from .oauth import get_oauth_token, MAL_TOKEN_URL, ANILIST_TOKEN_URL, KITSU_TOKEN_URL
|
|
|
|
bp = Blueprint('aes', __name__, url_prefix='/aes')
|
|
|
|
|
|
MAL_AUTHORIZE_URL = 'https://myanimelist.net/v1/oauth2/authorize'
|
|
ANILIST_AUTHORIZE_URL = 'https://anilist.co/api/v2/oauth/authorize'
|
|
|
|
BROKEN_MAL = ['35459', '27821', '31561', '38360', '36480', '40807']
|
|
|
|
BROKEN_KITSU = ['42279', '42090', '41963']
|
|
|
|
BROKEN_ANILIST = ['16932', '106169']
|
|
|
|
KITSU_MAL_FIX = {
|
|
'13298': '37897',
|
|
'42196': '39551',
|
|
'42867': '40852',
|
|
'13066': '34662',
|
|
'12770': '34451',
|
|
'8791': '21495'
|
|
}
|
|
|
|
KITSU_ANILIST_FIX = {
|
|
'41963': '104243',
|
|
'12770': '97886',
|
|
'8791': '102471',
|
|
'43142': '117756'
|
|
}
|
|
|
|
|
|
def generate_pkce():
|
|
"""Generate random string"""
|
|
letters = ascii_lowercase + digits
|
|
return ''.join(choice(letters) for i in range(100))
|
|
|
|
|
|
def hash_dict(dic):
|
|
"""Hash dict"""
|
|
json = dumps(dic)
|
|
return sha256(json.encode()).hexdigest()
|
|
|
|
|
|
def at_least(count, iterable):
|
|
"""Return True if bool(x) is True for at least `count` values x in the `iterable`."""
|
|
i = 0
|
|
for elem in iterable:
|
|
if elem:
|
|
i += 1
|
|
if i == count:
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_tokens(state):
|
|
"""Return dict of tokens"""
|
|
ret = {}
|
|
for key in state:
|
|
if '_token' in key:
|
|
ret[key.rstrip('_token')] = state[key]
|
|
return ret
|
|
|
|
|
|
@bp.route('/', methods=('GET', 'POST'))
|
|
def index():
|
|
"""AnimeEasySync index page"""
|
|
if 'state' in session:
|
|
state = session['state']
|
|
else:
|
|
state = dict()
|
|
session['pkce'] = generate_pkce()
|
|
can_sync = at_least(2, (bool(state.get(x))
|
|
for x in ['mal_token', 'anilist_token', 'kitsu_token']))
|
|
form = SyncForm()
|
|
if form.validate_on_submit() and can_sync and form.disc.data:
|
|
try:
|
|
disc_data = b64decode(
|
|
form.disc.data.encode('ascii')).decode('utf-8')
|
|
disc_lst = JSONDecoder(
|
|
object_hook=from_json).decode(disc_data)
|
|
tokens = {
|
|
'mal': state.get('mal_token'),
|
|
'anilist': state.get('anilist_token'),
|
|
'kitsu': state.get('kitsu_token')
|
|
}
|
|
source = ''
|
|
if form.mal.data:
|
|
source = 'mal'
|
|
elif form.anilist.data:
|
|
source = 'anilist'
|
|
elif form.kitsu.data:
|
|
source = 'kitsu'
|
|
if source and tokens[source]:
|
|
target_tokens_dict = {k: tokens[k]
|
|
for k in tokens if k != source}
|
|
sync_anime(disc_lst, source, target_tokens_dict)
|
|
except Exception as err:
|
|
print(err)
|
|
|
|
discrepancies = DiscrepancyCollection()
|
|
disc = b''
|
|
|
|
if can_sync:
|
|
if 'kitsu_user_id' in state:
|
|
kitsu_user_id = state['kitsu_user_id']
|
|
elif 'kitsu_token' in state:
|
|
kitsu_user_id = get_kitsu_user_id(
|
|
state['kitsu_token'])
|
|
state['kitsu_user_id'] = kitsu_user_id
|
|
mal_entries = read_mal(
|
|
state['mal_token']) if 'mal_token' in state else []
|
|
anilist_entries = read_anilist(
|
|
state['anilist_token']) if 'anilist_token' in state else []
|
|
kitsu_entries = read_kitsu(
|
|
state['kitsu_token'], kitsu_user_id) if 'kitsu_token' in state else []
|
|
discrepancies = get_discrepancies(
|
|
mal_entries, anilist_entries, kitsu_entries)
|
|
disc = b64encode(
|
|
dumps(discrepancies.get(False), cls=AESEncoder).encode('utf-8'))
|
|
|
|
for token_name in ['mal_token', 'anilist_token', 'kitsu_token']:
|
|
if token_name in state and not state.get(token_name):
|
|
del state[token_name]
|
|
|
|
session['state'] = state
|
|
|
|
mal_params = {
|
|
'response_type': 'code',
|
|
'client_id': current_app.config['MAL_CLIENT_ID'],
|
|
'state': hash_dict(state),
|
|
'redirect_uri': url_for('aes.mal_callback', _external=True),
|
|
'code_challenge': session['pkce'],
|
|
'code_challenge_method': 'plain'
|
|
}
|
|
mal_url = Request('GET', MAL_AUTHORIZE_URL,
|
|
params=mal_params).prepare().url
|
|
|
|
anilist_params = {
|
|
'response_type': 'code',
|
|
'client_id': current_app.config['ANILIST_CLIENT_ID'],
|
|
'redirect_uri': url_for('aes.anilist_callback', _external=True)
|
|
}
|
|
anilist_url = Request('GET', ANILIST_AUTHORIZE_URL,
|
|
params=anilist_params).prepare().url
|
|
|
|
modals_lst = []
|
|
if 'mal_token' in state:
|
|
modals_lst.append('MAL')
|
|
if 'anilist_token' in state:
|
|
modals_lst.append('Anilist')
|
|
if 'kitsu_token' in state:
|
|
modals_lst.append('Kitsu')
|
|
modals = read_modals(discrepancies, modals_lst)
|
|
form.disc.data = disc.decode('ascii')
|
|
|
|
return render_template('aes/index.jinja', state=state, mal_url=mal_url,
|
|
anilist_url=anilist_url, discrepancies=discrepancies.get(),
|
|
modals=modals, form=form)
|
|
|
|
|
|
@bp.route('/callback/anilist')
|
|
def anilist_callback():
|
|
"""AnimeEasySync Anilist callback"""
|
|
code = request.args.get('code', default=None, type=str)
|
|
state = dict()
|
|
if 'state' in session:
|
|
state = session['state']
|
|
if code:
|
|
refresh_token = None
|
|
if 'anilist_token' in state:
|
|
refresh_token = state['anilist_token'].get('refresh_token')
|
|
token = get_oauth_token(ANILIST_TOKEN_URL, 'authorization_code', refresh_token,
|
|
client_id=current_app.config['ANILIST_CLIENT_ID'],
|
|
client_secret=current_app.config['ANILIST_CLIENT_SECRET'],
|
|
redirect_uri=url_for(
|
|
'aes.anilist_callback', _external=True),
|
|
code=code)
|
|
if token:
|
|
state['anilist_token'] = token
|
|
session['state'] = state
|
|
return redirect(url_for('index'))
|
|
|
|
|
|
@bp.route('/callback/mal')
|
|
def mal_callback():
|
|
"""AnimeEasySync MAL callback"""
|
|
code = request.args.get('code', default=None, type=str)
|
|
state_hash = request.args.get('state', default=None, type=str)
|
|
state = dict()
|
|
if 'state' in session:
|
|
state = session['state']
|
|
if code and state_hash and state_hash == hash_dict(state):
|
|
pkce = ''
|
|
if 'pkce' in session:
|
|
pkce = session['pkce']
|
|
auth = HTTPBasicAuth(current_app.config['MAL_CLIENT_ID'],
|
|
current_app.config['MAL_CLIENT_SECRET'])
|
|
refresh_token = None
|
|
if 'mal_token' in state:
|
|
refresh_token = state['mal_token'].get('refresh_token')
|
|
token = get_oauth_token(MAL_TOKEN_URL, 'authorization_code', refresh_token, auth=auth,
|
|
client_id=current_app.config['MAL_CLIENT_ID'], code=code,
|
|
redirect_uri=url_for(
|
|
'aes.mal_callback', _external=True),
|
|
code_verifier=pkce)
|
|
if token:
|
|
state['mal_token'] = token
|
|
session['state'] = state
|
|
return redirect(url_for('index'))
|
|
|
|
|
|
@bp.route('/kitsu', methods=['GET', 'POST'])
|
|
def kitsu():
|
|
"""Login page for Kitsu"""
|
|
state = {}
|
|
if 'state' in session:
|
|
state = session['state']
|
|
if 'kitsu_token' in state:
|
|
return redirect(url_for('index'))
|
|
form = KitsuForm()
|
|
if form.validate_on_submit():
|
|
token = get_oauth_token(KITSU_TOKEN_URL, 'password', None,
|
|
username=form.username.data, password=form.password.data)
|
|
if token:
|
|
state['kitsu_token'] = token
|
|
session['state'] = state
|
|
if 'kitsu_token' in state:
|
|
return redirect(url_for('index'))
|
|
return render_template('aes/kitsu.jinja', form=form)
|
|
|
|
|
|
def sync_valid(src, trgt):
|
|
"""Check if entries are valid to sync"""
|
|
return src != trgt and is_valid_id(src.anime_id) and is_valid_id(trgt.anime_id)
|
|
|
|
|
|
def get_update_list(disc_lst, source, target):
|
|
"""Gets list of update parameters"""
|
|
ret = []
|
|
for dis in [ent for ent in disc_lst if sync_valid(ent.entries[source], ent.entries[target])]:
|
|
kwargs = {}
|
|
if dis.entries[source].score != dis.entries[target].score:
|
|
kwargs['score'] = dis.entries[source].score
|
|
if dis.entries[source].status != dis.entries[target].status:
|
|
kwargs['status'] = dis.entries[source].status
|
|
if 'completed' not in [dis.entries[source].status, dis.entries[target].status] and \
|
|
dis.entries[source].progress != dis.entries[target].progress:
|
|
kwargs['progress'] = dis.entries[source].progress
|
|
anime_id = dis.entries[target].library_entry_id if hasattr(
|
|
dis.entries[target], 'library_entry_id') else dis.entries[target].anime_id
|
|
ret.append((anime_id, kwargs))
|
|
return ret
|
|
|
|
|
|
def get_update_func(target):
|
|
"""Gets an update func"""
|
|
return UPDATE_FUNCS.get(target)
|
|
|
|
|
|
def sync_anime(discrepancies, source, target_tokens_dict):
|
|
"""Sync anime to source"""
|
|
for target in target_tokens_dict:
|
|
update = get_update_func(target)
|
|
if update:
|
|
for anime_id, kwargs in get_update_list(discrepancies, source, target):
|
|
update(target_tokens_dict[target], anime_id, **kwargs)
|
|
|
|
|
|
class KitsuForm(FlaskForm):
|
|
"""Kitsu login form"""
|
|
username = StringField('Email/Username', validators=[DataRequired()])
|
|
password = PasswordField('Password', validators=[DataRequired()])
|
|
submit = SubmitField('Login')
|
|
|
|
|
|
class SyncForm(FlaskForm):
|
|
"""Sync form"""
|
|
disc = HiddenField('disc', validators=[DataRequired()])
|
|
mal = SubmitField('Sync to MAL values')
|
|
anilist = SubmitField('Sync to Anilist values')
|
|
kitsu = SubmitField('Sync to Kitsu values')
|
|
|
|
|
|
def from_json(json_object):
|
|
"""AnimeEasySync JSON decoder"""
|
|
if isinstance(json_object, dict) and '__type__' in json_object:
|
|
obj_type = json_object['__type__']
|
|
if obj_type in globals():
|
|
obj_class = globals()[obj_type]
|
|
if obj_type == 'Anime':
|
|
ret = obj_class(from_json(json_object['anime_id']))
|
|
elif obj_type == 'KitsuAnime':
|
|
ret = obj_class(from_json(json_object['library_entry_id']), from_json(json_object['anime_id']))
|
|
else:
|
|
ret = obj_class()
|
|
for key in ret.__dict__:
|
|
setattr(ret, key, from_json(json_object[key]))
|
|
return ret
|
|
return json_object
|
|
|
|
|
|
class AESEncoder(JSONEncoder):
|
|
"""AnimeEasySync JSON encoder"""
|
|
|
|
def default(self, o):
|
|
if isinstance(o, (Anime, Discrepancy, DiscrepancyCollection, KitsuAnime)):
|
|
ret = {'__type__': o.__class__.__name__}
|
|
for key in o.__dict__:
|
|
if isinstance(o.__dict__[key], list):
|
|
lst = []
|
|
for elem in o.__dict__[key]:
|
|
lst.append(self.default(elem))
|
|
ret[key] = lst
|
|
else:
|
|
ret[key] = self.default(o.__dict__[key])
|
|
return ret
|
|
if isinstance(o, (int, str, float, dict)):
|
|
return o
|
|
if isinstance(o, list):
|
|
ret = []
|
|
for elem in o:
|
|
ret.append(self.default(elem))
|
|
return ret
|
|
print(o)
|
|
return super().default(o)
|
|
|
|
|
|
class Anime:
|
|
"""Class representing Anime entry"""
|
|
|
|
def __init__(self, anime_id):
|
|
self.anime_id = anime_id
|
|
self.title = ''
|
|
self.broken = False
|
|
self.progress = 'none'
|
|
self.score = 0
|
|
self.status = 'none'
|
|
self.link = ''
|
|
|
|
def fill(self, entry, broken):
|
|
"""Fill fields with data"""
|
|
self.progress = entry['progress']
|
|
self.score = entry['score']
|
|
self.status = entry['status']
|
|
self.title = entry['title']
|
|
self.link = entry['link']
|
|
self.broken = broken
|
|
|
|
def __eq__(self, other):
|
|
check_progress = self.progress == other.progress
|
|
check_status = self.status == other.status
|
|
check_score = self.score == other.score
|
|
all_completed = self.status == 'completed' and check_status
|
|
if not (check_progress or all_completed):
|
|
return False
|
|
if not check_score:
|
|
return False
|
|
if not check_status:
|
|
return False
|
|
return True
|
|
|
|
|
|
class KitsuAnime(Anime):
|
|
"""Class representing Kitsu Anime entry"""
|
|
|
|
def __init__(self, library_entry_id, *args):
|
|
super().__init__(*args)
|
|
self.library_entry_id = library_entry_id
|
|
|
|
|
|
def is_valid_id(anime_id):
|
|
"""Check if anime id is valid"""
|
|
return anime_id and int(anime_id) > 0
|
|
|
|
|
|
class Discrepancy:
|
|
"""Class representing a discrepancy"""
|
|
|
|
def __init__(self):
|
|
self.entries = {}
|
|
|
|
def fill_mal(self, mal_entry):
|
|
"""Fill fields from MAL entry"""
|
|
entry = Anime(mal_entry['mal_id'])
|
|
entry.fill(mal_entry, mal_entry['mal_id'] in BROKEN_MAL)
|
|
self.entries['mal'] = entry
|
|
|
|
def fill_anilist(self, anilist_entry):
|
|
"""Fill fields from Anilist entry"""
|
|
mal_entry = self.entries.get('mal')
|
|
if not mal_entry and anilist_entry['mal_id']:
|
|
mal_entry = Anime(anilist_entry['mal_id'])
|
|
self.entries['mal'] = mal_entry
|
|
entry = Anime(anilist_entry['anilist_id'])
|
|
entry.fill(anilist_entry,
|
|
anilist_entry['anilist_id'] in BROKEN_ANILIST)
|
|
self.entries['anilist'] = entry
|
|
|
|
def fill_kitsu(self, kitsu_entry):
|
|
"""Fill fields from Kitsu entry"""
|
|
mal_entry = self.entries.get('mal')
|
|
if not mal_entry and kitsu_entry['mal_id']:
|
|
mal_entry = Anime(kitsu_entry['mal_id'])
|
|
self.entries['mal'] = mal_entry
|
|
anilist_entry = self.entries.get('anilist')
|
|
if not anilist_entry and kitsu_entry['anilist_id']:
|
|
anilist_entry = Anime(kitsu_entry['anilist_id'])
|
|
self.entries['anilist'] = anilist_entry
|
|
entry = KitsuAnime(kitsu_entry['library_entry_id'],
|
|
kitsu_entry['kitsu_id'])
|
|
entry.fill(kitsu_entry, kitsu_entry['kitsu_id'] in BROKEN_KITSU)
|
|
self.entries['kitsu'] = entry
|
|
|
|
def is_discrepancy(self):
|
|
"""Check if there is a discrepancy"""
|
|
if any(not is_valid_id(x.anime_id) for x in self.entries.values()):
|
|
return True
|
|
if not self.entries.values():
|
|
return False
|
|
return any(x != list(self.entries.values())[0] for x in self.entries.values())
|
|
|
|
def is_broken(self):
|
|
"""Gets broken status"""
|
|
return any(x.broken for x in self.entries.values())
|
|
|
|
def get_title(self):
|
|
"""Gets anime title"""
|
|
return next((x.title for x in self.entries.values() if x.title), '')
|
|
|
|
|
|
class DiscrepancyCollection:
|
|
"""Class representing a collection of Discrepancies"""
|
|
|
|
def __init__(self):
|
|
self.collection = []
|
|
self.kitsu = {}
|
|
self.anilist = {}
|
|
self.mal = {}
|
|
|
|
def update_mal(self, mal_entry):
|
|
"""Add or update a MAL entry to collection"""
|
|
if mal_entry['mal_id'] in KITSU_MAL_FIX.values():
|
|
mal_entry['kitsu_id'] = list(KITSU_MAL_FIX.keys())[list(
|
|
KITSU_MAL_FIX.values()).index(mal_entry['mal_id'])]
|
|
if mal_entry['mal_id'] in self.mal:
|
|
entry = self.collection[self.mal[mal_entry['mal_id']]]
|
|
else:
|
|
entry = Discrepancy()
|
|
self.collection.append(entry)
|
|
self.mal[mal_entry['mal_id']] = len(self.collection) - 1
|
|
entry.fill_mal(mal_entry)
|
|
|
|
def update_anilist(self, anilist_entry):
|
|
"""Add or update a Anilist entry to collection"""
|
|
if anilist_entry['anilist_id'] in self.anilist:
|
|
entry = self.collection[self.anilist[anilist_entry['anilist_id']]]
|
|
elif anilist_entry['mal_id'] and anilist_entry['mal_id'] in self.mal:
|
|
entry = self.collection[self.mal[anilist_entry['mal_id']]]
|
|
self.anilist[anilist_entry['anilist_id']
|
|
] = self.mal[anilist_entry['mal_id']]
|
|
else:
|
|
entry = Discrepancy()
|
|
self.collection.append(entry)
|
|
if anilist_entry['mal_id']:
|
|
self.mal[anilist_entry['mal_id']] = len(self.collection) - 1
|
|
self.anilist[anilist_entry['anilist_id']] = len(
|
|
self.collection) - 1
|
|
entry.fill_anilist(anilist_entry)
|
|
|
|
def update_kitsu(self, kitsu_entry):
|
|
"""Add or update a Kitsu entry to collection"""
|
|
# WORKAROUND: fix broken ids
|
|
if kitsu_entry['kitsu_id'] in KITSU_ANILIST_FIX:
|
|
kitsu_entry['anilist_id'] = KITSU_ANILIST_FIX[kitsu_entry['kitsu_id']]
|
|
if kitsu_entry['kitsu_id'] in KITSU_MAL_FIX:
|
|
kitsu_entry['mal_id'] = KITSU_MAL_FIX[kitsu_entry['kitsu_id']]
|
|
|
|
if kitsu_entry['kitsu_id'] in self.kitsu:
|
|
entry = self.collection[self.anilist[kitsu_entry['kitsu_id']]]
|
|
if kitsu_entry['anilist_id'] in self.anilist:
|
|
entry = self.collection[self.anilist[kitsu_entry['anilist_id']]]
|
|
self.kitsu[kitsu_entry['kitsu_id']
|
|
] = self.anilist[kitsu_entry['anilist_id']]
|
|
if kitsu_entry['mal_id'] and kitsu_entry['mal_id'] not in self.mal:
|
|
self.mal[kitsu_entry['mal_id']
|
|
] = self.anilist[kitsu_entry['anilist_id']]
|
|
elif kitsu_entry['mal_id'] and kitsu_entry['mal_id'] in self.mal:
|
|
entry = self.collection[self.mal[kitsu_entry['mal_id']]]
|
|
self.kitsu[kitsu_entry['kitsu_id']
|
|
] = self.mal[kitsu_entry['mal_id']]
|
|
if kitsu_entry['anilist_id']:
|
|
self.anilist[kitsu_entry['anilist_id']
|
|
] = self.mal[kitsu_entry['mal_id']]
|
|
else:
|
|
entry = Discrepancy()
|
|
self.collection.append(entry)
|
|
if kitsu_entry['mal_id']:
|
|
self.mal[kitsu_entry['mal_id']] = len(self.collection) - 1
|
|
if kitsu_entry['anilist_id']:
|
|
self.anilist[kitsu_entry['anilist_id']] = len(
|
|
self.collection) - 1
|
|
self.kitsu[kitsu_entry['kitsu_id']] = len(self.collection) - 1
|
|
entry.fill_kitsu(kitsu_entry)
|
|
|
|
def get(self, with_broken=True):
|
|
"""Get entries that are broken or with a discrepancy"""
|
|
if with_broken:
|
|
return [x for x in self.collection if x.is_discrepancy() or x.is_broken()]
|
|
return [x for x in self.collection if x.is_discrepancy() and not x.is_broken()]
|
|
|
|
|
|
def get_discrepancies(mal_entries, anilist_entries, kitsu_entries):
|
|
"""Get discrepancies between entries"""
|
|
discrepancies = DiscrepancyCollection()
|
|
for entry in mal_entries:
|
|
discrepancies.update_mal(entry)
|
|
for entry in anilist_entries:
|
|
discrepancies.update_anilist(entry)
|
|
for entry in kitsu_entries:
|
|
discrepancies.update_kitsu(entry)
|
|
return discrepancies
|
|
|
|
|
|
def read_modals(discrepancies, lst):
|
|
"""Get modal info"""
|
|
ret = []
|
|
disc = discrepancies.get(False)
|
|
for target in lst:
|
|
src_lst = list(lst)
|
|
src_lst.remove(target)
|
|
ret.append({
|
|
'type': target,
|
|
'msg': changes_syncing(disc, target, *src_lst)
|
|
})
|
|
return ret
|
|
|
|
|
|
def changes_syncing_to_mal(lst):
|
|
"""Get changes to lists while syncing to MAL"""
|
|
return changes_syncing(lst, 'MAL', 'Anilist', 'Kitsu')
|
|
|
|
|
|
def changes_syncing(lst, target, *args):
|
|
"""Get changes to lists while syncinc to target"""
|
|
ret = ''
|
|
trgt_ent = target.lower()
|
|
for src in args:
|
|
src_ent = src.lower()
|
|
broken = [ent for ent in lst if src_ent not in ent.entries]
|
|
if broken:
|
|
print('broken entries detected:')
|
|
for ent in broken:
|
|
print(dumps(ent, cls=AESEncoder))
|
|
lst.remove(ent)
|
|
|
|
entries = [ent for ent in lst if ent.entries[trgt_ent]
|
|
!= ent.entries[src_ent]]
|
|
if entries:
|
|
ret += f'<h3>Changes to {src}:</h3><ul>'
|
|
for dis in entries:
|
|
if not sync_valid(dis.entries[src_ent], dis.entries[trgt_ent]):
|
|
continue
|
|
changes = ['<li><h4>', dis.get_title(), '</h4><ul>']
|
|
get_changes(
|
|
changes, dis.entries[src_ent], dis.entries[trgt_ent])
|
|
ret += ''.join(changes)
|
|
ret += '</ul></li>'
|
|
ret += '</ul>'
|
|
return ret
|
|
|
|
|
|
def changes_syncing_to_anilist(lst):
|
|
"""Get changes to lists while syncing to Anilist"""
|
|
return changes_syncing(lst, 'Anilist', 'MAL', 'Kitsu')
|
|
|
|
|
|
def changes_syncing_to_kitsu(lst):
|
|
"""Get changes to lists while syncing to Kitsu"""
|
|
return changes_syncing(lst, 'Kitsu', 'MAL', 'Anilist')
|
|
|
|
|
|
def get_changes(changes, src, trgt):
|
|
"""Get changes between 2 entries"""
|
|
if not sync_valid(src, trgt):
|
|
return
|
|
if src.score != trgt.score:
|
|
changes.append(f'<li>Score: {src.score} -> {trgt.score}</li>')
|
|
if src.status != trgt.status:
|
|
changes.append(f'<li>Status: {src.status} -> {trgt.status}</li>')
|
|
if src.progress != trgt.progress and (src.status != 'completed' or trgt.status != 'completed'):
|
|
changes.append(
|
|
f'<li>Progress: {src.progress} -> {trgt.progress}</li>')
|