maloja/maloja/utilities/images.py

398 lines
11 KiB
Python
Raw Normal View History

from ..globalconf import data_dir, malojaconfig
2020-09-04 03:42:01 +03:00
from .. import thirdparty
2022-02-17 09:35:05 +03:00
from .. import database
2020-09-04 03:42:01 +03:00
from doreah import caching
2020-09-04 03:42:01 +03:00
from doreah.logging import log
import itertools
import os
import urllib
import random
import base64
2020-09-04 03:42:01 +03:00
from threading import Thread, Timer
import re
2020-09-04 14:59:04 +03:00
import datetime
2022-02-17 09:35:05 +03:00
import sqlalchemy as sql
2018-12-17 17:10:10 +03:00
2022-02-17 09:35:05 +03:00
DB = {}
engine = sql.create_engine(f"sqlite:///{data_dir['cache']('images.sqlite')}", echo = False)
meta = sql.MetaData()
DB['artists'] = sql.Table(
'artists', meta,
sql.Column('id',sql.Integer,primary_key=True),
sql.Column('url',sql.String),
sql.Column('expire',sql.Integer)
)
DB['tracks'] = sql.Table(
'tracks', meta,
sql.Column('id',sql.Integer,primary_key=True),
sql.Column('url',sql.String),
sql.Column('expire',sql.Integer)
)
meta.create_all(engine)
def get_image_from_cache(id,table):
now = int(datetime.datetime.now().timestamp())
with engine.begin() as conn:
op = DB[table].select(
DB[table].c.url
).where(
DB[table].c.id==id,
DB[table].c.expire>now
)
result = conn.execute(op).all()
for row in result:
return row.url
def set_image_in_cache(id,table,url):
now = int(datetime.datetime.now().timestamp())
if url is None:
expire = now + (malojaconfig["CACHE_EXPIRE_NEGATIVE"] * 24 * 3600)
else:
expire = now + (malojaconfig["CACHE_EXPIRE_POSITIVE"] * 24 * 3600)
with engine.begin() as conn:
op = DB[table].insert().values(
id=id,
url=url,
expire=expire
).prefix_with('OR IGNORE')
result = conn.execute(op)
def get_track_image(track=None,track_id=None,fast=False):
if track_id is None:
track_id = database.sqldb.get_track_id(track)
title = track['title']
artists = track['artists']
# check cache
result = get_image_from_cache(track_id,'tracks')
if result is not None:
return result
# local image
if malojaconfig["USE_LOCAL_IMAGES"]:
images = local_files(artists=artists,title=title)
if len(images) != 0:
result = random.choice(images)
result = urllib.parse.quote(result)
set_image_in_cache(track_id,'tracks',result)
return result
# forward
if fast:
titlequery = "title=" + urllib.parse.quote(title)
artistquery = "&".join("artist=" + urllib.parse.quote(a) for a in artists)
return (f"/image?{titlequery}&{artistquery}")
# third party
result = thirdparty.get_image_track_all((artists,title))
set_image_in_cache(track_id,'tracks',result)
if result is not None: return result
for a in artists:
res = get_artist_image(artist=a,fast=False)
if res != "": return res
return None
def get_artist_image(artist=None,artist_id=None,fast=False):
if artist_id is None:
artist_id = database.sqldb.get_artist_id(artist)
# check cache
result = get_image_from_cache(artist_id,'artists')
if result is not None:
return result
# local image
if malojaconfig["USE_LOCAL_IMAGES"]:
images = local_files(artist=artist)
if len(images) != 0:
result = random.choice(images)
result = urllib.parse.quote(result)
set_image_in_cache(artist_id,'artists',result)
return result
# forward
if fast:
artistquery = "artist=" + urllib.parse.quote(artist)
return (f"/image?{artistquery}")
# third party
result = thirdparty.get_image_artist_all(artist)
set_image_in_cache(artist_id,'artists',result)
return result
2019-04-01 17:52:42 +03:00
### Caches
2021-12-19 23:10:55 +03:00
cacheage = malojaconfig["CACHE_EXPIRE_POSITIVE"] * 24 * 3600
cacheage_neg = malojaconfig["CACHE_EXPIRE_NEGATIVE"] * 24 * 3600
2019-04-15 13:26:12 +03:00
artist_cache = caching.Cache(name="imgcache_artists",maxage=cacheage,maxage_negative=cacheage_neg,persistent=True)
track_cache = caching.Cache(name="imgcache_tracks",maxage=cacheage,maxage_negative=cacheage_neg,persistent=True)
2019-04-03 17:03:48 +03:00
# removes emojis and weird shit from names
def clean(name):
return "".join(c for c in name if c.isalnum() or c in []).strip()
def get_all_possible_filenames(artist=None,artists=None,title=None):
2019-04-03 17:03:48 +03:00
# check if we're dealing with a track or artist, then clean up names
# (only remove non-alphanumeric, allow korean and stuff)
2019-04-03 17:03:48 +03:00
if title is not None and artists is not None:
track = True
title, artists = clean(title), [clean(a) for a in artists]
elif artist is not None:
track = False
artist = clean(artist)
else: return []
superfolder = "tracks/" if track else "artists/"
2019-04-03 17:03:48 +03:00
filenames = []
if track:
#unsafeartists = [artist.translate(None,"-_./\\") for artist in artists]
safeartists = [re.sub("[^a-zA-Z0-9]","",artist) for artist in artists]
#unsafetitle = title.translate(None,"-_./\\")
safetitle = re.sub("[^a-zA-Z0-9]","",title)
if len(artists) < 4:
unsafeperms = itertools.permutations(artists)
safeperms = itertools.permutations(safeartists)
else:
unsafeperms = [sorted(artists)]
safeperms = [sorted(safeartists)]
for unsafeartistlist in unsafeperms:
filename = "-".join(unsafeartistlist) + "_" + title
if filename != "":
filenames.append(filename)
filenames.append(filename.lower())
for safeartistlist in safeperms:
filename = "-".join(safeartistlist) + "_" + safetitle
if filename != "":
filenames.append(filename)
filenames.append(filename.lower())
filenames = list(set(filenames))
if len(filenames) == 0: filenames.append(str(hash((frozenset(artists),title))))
else:
#unsafeartist = artist.translate(None,"-_./\\")
safeartist = re.sub("[^a-zA-Z0-9]","",artist)
filename = artist
if filename != "":
filenames.append(filename)
filenames.append(filename.lower())
filename = safeartist
if filename != "":
filenames.append(filename)
filenames.append(filename.lower())
filenames = list(set(filenames))
if len(filenames) == 0: filenames.append(str(hash(artist)))
return [superfolder + name for name in filenames]
def local_files(artist=None,artists=None,title=None):
filenames = get_all_possible_filenames(artist,artists,title)
2019-04-03 17:03:48 +03:00
images = []
for purename in filenames:
# direct files
for ext in ["png","jpg","jpeg","gif"]:
#for num in [""] + [str(n) for n in range(0,10)]:
if os.path.exists(data_dir['images'](purename + "." + ext)):
images.append("/images/" + purename + "." + ext)
2019-04-03 17:03:48 +03:00
# folder
try:
for f in os.listdir(data_dir['images'](purename)):
2019-04-03 17:03:48 +03:00
if f.split(".")[-1] in ["png","jpg","jpeg","gif"]:
images.append("/images/" + purename + "/" + f)
2019-04-03 17:03:48 +03:00
except:
pass
return images
# these caches are there so we don't check all files every time, but return the same one
2021-12-19 23:10:55 +03:00
local_cache_age = malojaconfig["LOCAL_IMAGE_ROTATE"]
2019-04-03 17:43:09 +03:00
local_artist_cache = caching.Cache(maxage=local_cache_age)
local_track_cache = caching.Cache(maxage=local_cache_age)
2019-03-12 13:39:36 +03:00
def getTrackImage(artists,title,fast=False):
2019-03-06 20:04:12 +03:00
2021-12-09 23:41:57 +03:00
hashable_track = (frozenset(artists),title)
2019-04-07 14:22:33 +03:00
2021-12-09 23:41:57 +03:00
# Prio 1: Local image
2021-12-19 23:10:55 +03:00
if malojaconfig["USE_LOCAL_IMAGES"]:
2019-04-07 14:22:33 +03:00
try:
2021-12-21 09:11:24 +03:00
return local_track_cache.get(hashable_track)
2019-04-07 14:22:33 +03:00
except:
images = local_files(artists=artists,title=title)
if len(images) != 0:
res = random.choice(images)
2021-12-09 23:41:57 +03:00
local_track_cache.add(hashable_track,res)
2019-04-07 14:22:33 +03:00
return urllib.parse.quote(res)
2019-04-03 17:03:48 +03:00
2021-12-09 23:41:57 +03:00
# Prio 2: Cached remote link
2018-12-28 20:06:09 +03:00
try:
2021-12-09 23:41:57 +03:00
result = track_cache.get(hashable_track)
2021-12-21 09:11:24 +03:00
if result is not None: return result
2021-12-09 23:41:57 +03:00
# if we have cached the nonexistence of that image, we immediately return
# the redirect to the artist and let the resolver handle it
2019-03-12 13:39:36 +03:00
# (even if we're not in a fast lookup right now)
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
for a in artists:
res = getArtistImage(artist=a,fast=True)
if res != "": return res
return ""
2018-12-28 20:06:09 +03:00
except:
pass
2021-12-09 23:41:57 +03:00
# fast request will not go further than this, but now generate redirect link
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
if fast:
return ("/image?title=" + urllib.parse.quote(title) + "&" + "&".join(
"artist=" + urllib.parse.quote(a) for a in artists))
2021-12-09 23:41:57 +03:00
# Prio 3 (only non-fast lookup): actually call third parties
result = thirdparty.get_image_track_all((artists,title))
2019-03-12 13:39:36 +03:00
# cache results (even negative ones)
2021-12-09 23:41:57 +03:00
track_cache.add(hashable_track,result)
2019-03-12 13:39:36 +03:00
# return either result or redirect to artist
2021-12-21 09:11:24 +03:00
if result is not None: return result
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
for a in artists:
res = getArtistImage(artist=a,fast=False)
if res != "": return res
return ""
2019-03-12 13:39:36 +03:00
def getArtistImage(artist,fast=False):
2021-12-09 23:41:57 +03:00
# Prio 1: Local image
2021-12-19 23:10:55 +03:00
if malojaconfig["USE_LOCAL_IMAGES"]:
2019-04-07 14:22:33 +03:00
try:
2021-12-21 09:11:24 +03:00
return local_artist_cache.get(artist)
2019-04-07 14:22:33 +03:00
except:
images = local_files(artist=artist)
if len(images) != 0:
res = random.choice(images)
local_artist_cache.add(artist,res)
2021-12-21 09:11:24 +03:00
return urllib.parse.quote(res)
2021-12-09 23:41:57 +03:00
# Prio 2: Cached remote link
2018-12-17 17:10:10 +03:00
try:
result = artist_cache.get(artist)
2021-12-21 09:11:24 +03:00
if result is not None: return result
2019-03-12 13:39:36 +03:00
else: return ""
# none means non-existence is cached, return empty
2018-12-28 20:06:09 +03:00
except:
2018-12-17 17:10:10 +03:00
pass
# no cache entry, go on
2021-12-09 23:41:57 +03:00
# fast request will not go further than this, but now generate redirect link
2019-03-12 13:39:36 +03:00
if fast: return "/image?artist=" + urllib.parse.quote(artist)
2021-12-09 23:41:57 +03:00
# Prio 3 (only non-fast lookup): actually call third parties
result = thirdparty.get_image_artist_all(artist)
2019-03-12 13:39:36 +03:00
# cache results (even negative ones)
2019-04-01 17:52:42 +03:00
artist_cache.add(artist,result) #cache_artist(artist,result)
2021-12-21 09:11:24 +03:00
if result is not None: return result
2019-03-12 13:39:36 +03:00
else: return ""
2019-02-02 20:08:30 +03:00
2019-03-12 13:39:36 +03:00
def getTrackImages(trackobjectlist,fast=False):
2019-02-02 20:08:30 +03:00
threads = []
2019-02-02 20:08:30 +03:00
for track in trackobjectlist:
2019-03-12 13:39:36 +03:00
t = Thread(target=getTrackImage,args=(track["artists"],track["title"],),kwargs={"fast":fast})
2019-02-02 20:08:30 +03:00
t.start()
threads.append(t)
2019-02-02 20:08:30 +03:00
for t in threads:
t.join()
2019-03-12 13:39:36 +03:00
return [getTrackImage(t["artists"],t["title"]) for t in trackobjectlist]
2019-03-12 13:39:36 +03:00
def getArtistImages(artistlist,fast=False):
threads = []
for artist in artistlist:
2019-03-12 13:39:36 +03:00
t = Thread(target=getArtistImage,args=(artist,),kwargs={"fast":fast})
t.start()
threads.append(t)
for t in threads:
t.join()
# async calls only cached results, now we need to get them
2019-03-12 13:39:36 +03:00
return [getArtistImage(a) for a in artistlist]
2019-02-03 18:52:37 +03:00
# new way of serving images
# instead always generate a link locally, but redirect that on the fly
# this way the page can load faster and images will trickle in without having to resort to XHTTP requests
def resolveImage(artist=None,track=None):
if track is not None:
2019-03-12 13:39:36 +03:00
return getTrackImage(track["artists"],track["title"])
elif artist is not None:
2019-03-12 13:39:36 +03:00
return getArtistImage(artist)
def set_image(b64,**keys):
track = "title" in keys
2020-08-21 19:06:16 +03:00
log("Trying to set image, b64 string: " + str(b64[:30] + "..."),module="debug")
regex = r"data:image/(\w+);base64,(.+)"
type,b64 = re.fullmatch(regex,b64).groups()
b64 = base64.b64decode(b64)
filename = "webupload" + str(int(datetime.datetime.now().timestamp())) + "." + type
for folder in get_all_possible_filenames(**keys):
if os.path.exists(data_dir['images'](folder)):
with open(data_dir['images'](folder,filename),"wb") as f:
f.write(b64)
2021-01-16 22:11:06 +03:00
break
else:
folder = get_all_possible_filenames(**keys)[0]
os.makedirs(data_dir['images'](folder))
with open(data_dir['images'](folder,filename),"wb") as f:
f.write(b64)
2021-01-16 22:11:06 +03:00
log("Saved image as " + data_dir['images'](folder,filename),module="debug")
# set as current picture in rotation
2021-01-16 22:11:06 +03:00
if track: local_track_cache.add((frozenset(keys["artists"]),keys["title"]),os.path.join("/images",folder,filename))
else: local_artist_cache.add(keys["artist"],os.path.join("/images",folder,filename))