jellyfin-kodi/resources/lib/artwork.py

555 lines
20 KiB
Python
Raw Normal View History

2016-04-06 18:12:42 +00:00
# -*- coding: utf-8 -*-
#################################################################################################
import logging
2016-04-06 18:12:42 +00:00
import os
import urllib
from sqlite3 import OperationalError
import xbmc
import xbmcgui
import xbmcvfs
2016-09-10 11:15:58 +00:00
import requests
2016-04-06 18:12:42 +00:00
import image_cache_thread
from utils import window, settings, dialog, language as lang, JSONRPC
from database import DatabaseConn
from contextlib import closing
2016-04-06 18:12:42 +00:00
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
class Artwork(object):
2016-04-06 18:12:42 +00:00
xbmc_host = 'localhost'
xbmc_port = None
xbmc_username = None
xbmc_password = None
2016-09-10 11:15:58 +00:00
image_cache_threads = []
image_cache_limit = 0
2016-04-06 18:12:42 +00:00
2016-06-16 05:43:36 +00:00
2016-04-06 18:12:42 +00:00
def __init__(self):
2016-06-16 05:43:36 +00:00
2016-09-10 11:15:58 +00:00
self.enable_texture_cache = settings('enableTextureCache') == "true"
self.image_cache_limit = int(settings('imageCacheLimit')) * 5
2016-09-27 02:50:58 +00:00
log.debug("image cache thread count: %s", self.image_cache_limit)
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
if not self.xbmc_port and self.enable_texture_cache:
self._set_webserver_details()
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
self.user_id = window('emby_currUser')
self.server = window('emby_server%s' % self.user_id)
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
def _double_urlencode(self, text):
2016-09-17 13:29:22 +00:00
text = self.single_urlencode(text)
text = self.single_urlencode(text)
2016-04-06 18:12:42 +00:00
return text
2016-09-10 11:15:58 +00:00
@classmethod
2016-09-17 13:29:22 +00:00
def single_urlencode(cls, text):
2016-06-16 05:43:36 +00:00
# urlencode needs a utf- string
2016-09-10 11:15:58 +00:00
text = urllib.urlencode({'blahblahblah': text.encode('utf-8')})
2016-04-06 18:12:42 +00:00
text = text[13:]
2016-09-10 11:15:58 +00:00
return text.decode('utf-8') #return the result again as unicode
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
def _set_webserver_details(self):
2016-04-06 18:12:42 +00:00
# Get the Kodi webserver details - used to set the texture cache
2016-09-10 11:15:58 +00:00
get_setting_value = JSONRPC('Settings.GetSettingValue')
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
web_query = {
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
"setting": "services.webserver"
2016-04-06 18:12:42 +00:00
}
2016-09-10 11:15:58 +00:00
result = get_setting_value.execute(web_query)
2016-04-06 18:12:42 +00:00
try:
xbmc_webserver_enabled = result['result']['value']
2016-04-14 00:37:02 +00:00
except (KeyError, TypeError):
2016-04-06 18:12:42 +00:00
xbmc_webserver_enabled = False
if not xbmc_webserver_enabled:
# Enable the webserver, it is disabled
2016-09-10 11:15:58 +00:00
set_setting_value = JSONRPC('Settings.SetSettingValue')
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
web_port = {
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
"setting": "services.webserverport",
"value": 8080
2016-04-06 18:12:42 +00:00
}
2016-09-10 11:15:58 +00:00
set_setting_value.execute(web_port)
2016-04-06 18:12:42 +00:00
self.xbmc_port = 8080
web_user = {
2016-09-10 11:15:58 +00:00
"setting": "services.webserver",
"value": True
2016-04-06 18:12:42 +00:00
}
2016-09-10 11:15:58 +00:00
set_setting_value.execute(web_user)
2016-04-06 18:12:42 +00:00
self.xbmc_username = "kodi"
# Webserver already enabled
web_port = {
2016-09-10 11:15:58 +00:00
"setting": "services.webserverport"
2016-04-06 18:12:42 +00:00
}
2016-09-10 11:15:58 +00:00
result = get_setting_value.execute(web_port)
2016-04-06 18:12:42 +00:00
try:
self.xbmc_port = result['result']['value']
except (TypeError, KeyError):
2016-04-06 18:12:42 +00:00
pass
web_user = {
2016-09-10 11:15:58 +00:00
"setting": "services.webserverusername"
2016-04-06 18:12:42 +00:00
}
2016-09-10 11:15:58 +00:00
result = get_setting_value.execute(web_user)
2016-04-06 18:12:42 +00:00
try:
self.xbmc_username = result['result']['value']
2016-10-20 07:21:00 +00:00
except (TypeError, KeyError):
2016-04-06 18:12:42 +00:00
pass
web_pass = {
2016-09-10 11:15:58 +00:00
"setting": "services.webserverpassword"
2016-04-06 18:12:42 +00:00
}
2016-09-10 11:15:58 +00:00
result = get_setting_value.execute(web_pass)
2016-04-06 18:12:42 +00:00
try:
self.xbmc_password = result['result']['value']
2016-10-20 07:21:00 +00:00
except (TypeError, KeyError):
2016-04-06 18:12:42 +00:00
pass
2016-09-10 11:15:58 +00:00
def texture_cache_sync(self):
2016-04-06 18:12:42 +00:00
# This method will sync all Kodi artwork to textures13.db
# and cache them locally. This takes diskspace!
2016-09-10 11:15:58 +00:00
if not dialog(type_="yesno",
heading="{emby}",
line1=lang(33042)):
2016-04-06 18:12:42 +00:00
return
log.info("Doing Image Cache Sync")
2016-04-06 18:12:42 +00:00
2016-06-16 05:43:36 +00:00
pdialog = xbmcgui.DialogProgress()
2016-06-20 18:59:55 +00:00
pdialog.create(lang(29999), lang(33043))
2016-04-06 18:12:42 +00:00
# ask to rest all existing or not
2016-09-10 11:15:58 +00:00
if dialog(type_="yesno", heading="{emby}", line1=lang(33044)):
log.info("Resetting all cache data first")
2016-09-10 21:26:29 +00:00
self.delete_cache()
2016-04-06 18:12:42 +00:00
# Cache all entries in video DB
2016-09-10 11:15:58 +00:00
self._cache_all_video_entries(pdialog)
# Cache all entries in music DB
self._cache_all_music_entries(pdialog)
pdialog.update(100, "%s %s" % (lang(33046), len(self.image_cache_threads)))
log.info("Waiting for all threads to exit")
while len(self.image_cache_threads):
for thread in self.image_cache_threads:
if thread.is_finished:
self.image_cache_threads.remove(thread)
pdialog.update(100, "%s %s" % (lang(33046), len(self.image_cache_threads)))
log.info("Waiting for all threads to exit: %s", len(self.image_cache_threads))
xbmc.sleep(500)
pdialog.close()
def _cache_all_video_entries(self, pdialog):
with DatabaseConn('video') as conn:
with closing(conn.cursor()) as cursor_video:
cursor_video.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors
result = cursor_video.fetchall()
total = len(result)
log.info("Image cache sync about to process %s images", total)
cursor_video.close()
2016-06-16 05:43:36 +00:00
count = 0
for url in result:
2016-09-10 11:15:58 +00:00
if pdialog.iscanceled():
break
2016-06-16 05:43:36 +00:00
percentage = int((float(count) / float(total))*100)
message = "%s of %s (%s)" % (count, total, len(self.image_cache_threads))
pdialog.update(percentage, "%s %s" % (lang(33045), message))
self.cache_texture(url[0])
count += 1
2016-09-10 11:15:58 +00:00
def _cache_all_music_entries(self, pdialog):
with DatabaseConn('music') as conn:
with closing(conn.cursor()) as cursor_music:
cursor_music.execute("SELECT url FROM art")
result = cursor_music.fetchall()
total = len(result)
log.info("Image cache sync about to process %s images", total)
2016-06-16 05:43:36 +00:00
count = 0
for url in result:
2016-09-10 11:15:58 +00:00
if pdialog.iscanceled():
break
2016-06-16 05:43:36 +00:00
percentage = int((float(count) / float(total))*100)
message = "%s of %s" % (count, total)
pdialog.update(percentage, "%s %s" % (lang(33045), message))
self.cache_texture(url[0])
count += 1
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
@classmethod
2016-09-10 21:26:29 +00:00
def delete_cache(cls):
2016-09-10 11:15:58 +00:00
# Remove all existing textures first
path = xbmc.translatePath('special://thumbnails/').decode('utf-8')
if xbmcvfs.exists(path):
dirs, ignore_files = xbmcvfs.listdir(path)
for directory in dirs:
ignore_dirs, files = xbmcvfs.listdir(path + directory)
for file_ in files:
if os.path.supports_unicode_filenames:
filename = os.path.join(path + directory.decode('utf-8'),
file_.decode('utf-8'))
else:
filename = os.path.join(path.encode('utf-8') + directory, file_)
xbmcvfs.delete(filename)
2016-09-10 21:26:29 +00:00
log.debug("deleted: %s", filename)
2016-09-10 11:15:58 +00:00
# remove all existing data from texture DB
with DatabaseConn('texture') as conn:
with closing(conn.cursor()) as cursor_texture:
cursor_texture.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
rows = cursor_texture.fetchall()
for row in rows:
table_name = row[0]
if table_name != "version":
cursor_texture.execute("DELETE FROM " + table_name)
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
def _add_worker_image_thread(self, url):
2016-04-06 18:12:42 +00:00
2016-06-16 05:43:36 +00:00
while True:
2016-04-06 18:12:42 +00:00
# removed finished
2016-09-10 11:15:58 +00:00
for thread in self.image_cache_threads:
2016-08-22 02:51:23 +00:00
if thread.is_finished:
2016-09-10 11:15:58 +00:00
self.image_cache_threads.remove(thread)
2016-04-06 18:12:42 +00:00
# add a new thread or wait and retry if we hit our limit
2016-09-10 11:15:58 +00:00
if len(self.image_cache_threads) < self.image_cache_limit:
new_thread = image_cache_thread.ImageCacheThread()
new_thread.set_url(self._double_urlencode(url))
new_thread.set_host(self.xbmc_host, self.xbmc_port)
new_thread.set_auth(self.xbmc_username, self.xbmc_password)
new_thread.start()
self.image_cache_threads.append(new_thread)
2016-04-06 18:12:42 +00:00
return
else:
2016-09-10 11:15:58 +00:00
log.info("Waiting for empty queue spot: %s", len(self.image_cache_threads))
2016-04-06 18:12:42 +00:00
xbmc.sleep(50)
2016-09-10 11:15:58 +00:00
def cache_texture(self, url):
2016-04-06 18:12:42 +00:00
# Cache a single image url to the texture cache
2016-09-10 11:15:58 +00:00
if url and self.enable_texture_cache:
log.debug("Processing: %s", url)
if not self.image_cache_limit:
url = self._double_urlencode(url)
try: # Add image to texture cache by simply calling it at the http endpoint
requests.head(url=("http://%s:%s/image/image://%s"
% (self.xbmc_host, self.xbmc_port, url)),
auth=(self.xbmc_username, self.xbmc_password),
timeout=(0.01, 0.01))
except Exception: # We don't need the result
pass
2016-04-06 18:12:42 +00:00
else:
2016-09-10 11:15:58 +00:00
self._add_worker_image_thread(url)
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
def add_artwork(self, artwork, kodi_id, media_type, cursor):
2016-04-06 18:12:42 +00:00
# Kodi conversion table
2016-09-10 11:15:58 +00:00
kodi_artwork = {
2016-04-06 18:12:42 +00:00
'Primary': ["thumb", "poster"],
'Banner': "banner",
'Logo': "clearlogo",
'Art': "clearart",
'Thumb': "landscape",
'Disc': "discart",
'Backdrop': "fanart",
'BoxRear': "poster"
}
# Artwork is a dictionary
2016-09-10 11:15:58 +00:00
for artwork_type in artwork:
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
if artwork_type == 'Backdrop':
2016-04-06 18:12:42 +00:00
# Backdrop entry is a list
# Process extra fanart for artwork downloader (fanart, fanart1, fanart2...)
2016-09-10 11:15:58 +00:00
backdrops = artwork[artwork_type]
backdrops_number = len(backdrops)
2016-04-06 18:12:42 +00:00
query = ' '.join((
"SELECT url",
"FROM art",
"WHERE media_id = ?",
"AND media_type = ?",
"AND type LIKE ?"
))
2016-09-10 11:15:58 +00:00
cursor.execute(query, (kodi_id, media_type, "fanart%",))
2016-04-06 18:12:42 +00:00
rows = cursor.fetchall()
2016-09-10 11:15:58 +00:00
if len(rows) > backdrops_number:
2016-04-06 18:12:42 +00:00
# More backdrops in database. Delete extra fanart.
query = ' '.join((
"DELETE FROM art",
"WHERE media_id = ?",
"AND media_type = ?",
"AND type LIKE ?"
))
2016-09-10 11:15:58 +00:00
cursor.execute(query, (kodi_id, media_type, "fanart_",))
2016-04-06 18:12:42 +00:00
# Process backdrops and extra fanart
2016-09-10 11:15:58 +00:00
for index, backdrop in enumerate(backdrops):
self.add_update_art(image_url=backdrop,
kodi_id=kodi_id,
media_type=media_type,
image_type=("fanart" if not index else "%s%s"
% ("fanart", index)),
cursor=cursor)
elif artwork_type == 'Primary':
2016-04-06 18:12:42 +00:00
# Primary art is processed as thumb and poster for Kodi.
2016-09-10 11:15:58 +00:00
for art_type in kodi_artwork[artwork_type]:
self.add_update_art(image_url=artwork[artwork_type],
kodi_id=kodi_id,
media_type=media_type,
image_type=art_type,
cursor=cursor)
elif artwork_type in kodi_artwork:
2016-04-06 18:12:42 +00:00
# Process the rest artwork type that Kodi can use
2016-09-10 11:15:58 +00:00
self.add_update_art(image_url=artwork[artwork_type],
kodi_id=kodi_id,
media_type=media_type,
image_type=kodi_artwork[artwork_type],
cursor=cursor)
def add_update_art(self, image_url, kodi_id, media_type, image_type, cursor):
2016-04-06 18:12:42 +00:00
# Possible that the imageurl is an empty string
2016-09-10 11:15:58 +00:00
if image_url:
cache_image = False
2016-04-06 18:12:42 +00:00
query = ' '.join((
"SELECT url",
"FROM art",
"WHERE media_id = ?",
"AND media_type = ?",
"AND type = ?"
))
2016-09-10 11:15:58 +00:00
cursor.execute(query, (kodi_id, media_type, image_type,))
2016-04-06 18:12:42 +00:00
try: # Update the artwork
url = cursor.fetchone()[0]
except TypeError: # Add the artwork
2016-09-10 11:15:58 +00:00
cache_image = True
log.debug("Adding Art Link for kodiId: %s (%s)", kodi_id, image_url)
2016-04-06 18:12:42 +00:00
query = (
'''
INSERT INTO art(media_id, media_type, type, url)
VALUES (?, ?, ?, ?)
'''
)
2016-09-10 11:15:58 +00:00
cursor.execute(query, (kodi_id, media_type, image_type, image_url))
2016-04-06 18:12:42 +00:00
else: # Only cache artwork if it changed
2016-09-10 11:15:58 +00:00
if url != image_url:
cache_image = True
2016-04-06 18:12:42 +00:00
# Only for the main backdrop, poster
2016-06-16 05:43:36 +00:00
if (window('emby_initialScan') != "true" and
2016-09-10 11:15:58 +00:00
image_type in ("fanart", "poster")):
2016-04-06 18:12:42 +00:00
# Delete current entry before updating with the new one
2016-09-10 11:15:58 +00:00
self.delete_cached_artwork(url)
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
log.info("Updating Art url for %s kodiId: %s (%s) -> (%s)",
image_type, kodi_id, url, image_url)
2016-04-06 18:12:42 +00:00
query = ' '.join((
"UPDATE art",
"SET url = ?",
"WHERE media_id = ?",
"AND media_type = ?",
"AND type = ?"
))
2016-09-10 11:15:58 +00:00
cursor.execute(query, (image_url, kodi_id, media_type, image_type))
2016-04-06 18:12:42 +00:00
# Cache fanart and poster in Kodi texture cache
2016-09-10 11:15:58 +00:00
if cache_image and image_type in ("fanart", "poster"):
self.cache_texture(image_url)
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
def delete_artwork(self, kodi_id, media_type, cursor):
2016-04-06 18:12:42 +00:00
query = ' '.join((
"SELECT url, type",
"FROM art",
"WHERE media_id = ?",
"AND media_type = ?"
))
2016-09-10 11:15:58 +00:00
cursor.execute(query, (kodi_id, media_type,))
2016-04-06 18:12:42 +00:00
rows = cursor.fetchall()
for row in rows:
url = row[0]
2016-09-10 11:15:58 +00:00
image_type = row[1]
if image_type in ("poster", "fanart"):
self.delete_cached_artwork(url)
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
@classmethod
def delete_cached_artwork(cls, url):
# Only necessary to remove and apply a new backdrop or poster
with DatabaseConn('texture') as conn:
with closing(conn.cursor()) as cursor_texture:
try:
cursor_texture.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,))
cached_url = cursor_texture.fetchone()[0]
2016-04-06 18:12:42 +00:00
except TypeError:
log.info("Could not find cached url")
2016-04-06 18:12:42 +00:00
except OperationalError:
log.info("Database is locked. Skip deletion process.")
2016-04-06 18:12:42 +00:00
else: # Delete thumbnail as well as the entry
thumbnails = xbmc.translatePath("special://thumbnails/%s" % cached_url).decode('utf-8')
log.info("Deleting cached thumbnail: %s", thumbnails)
xbmcvfs.delete(thumbnails)
2016-04-06 18:12:42 +00:00
try:
cursor_texture.execute("DELETE FROM texture WHERE url = ?", (url,))
except OperationalError:
log.debug("Issue deleting url from cache. Skipping.")
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
def get_people_artwork(self, people):
2016-04-06 18:12:42 +00:00
# append imageurl if existing
for person in people:
image = ""
2016-09-10 11:15:58 +00:00
person_id = person['Id']
if "PrimaryImageTag" in person:
2016-04-06 18:12:42 +00:00
image = (
"%s/emby/Items/%s/Images/Primary?"
"MaxWidth=400&MaxHeight=400&Index=0&Tag=%s"
2016-09-10 11:15:58 +00:00
% (self.server, person_id, person['PrimaryImageTag']))
2016-04-06 18:12:42 +00:00
person['imageurl'] = image
return people
2016-09-10 11:15:58 +00:00
def get_user_artwork(self, item_id, item_type):
2016-04-06 18:12:42 +00:00
# Load user information set by UserClient
2016-09-10 11:15:58 +00:00
return "%s/emby/Users/%s/Images/%s?Format=original" % (self.server, item_id, item_type)
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
def get_all_artwork(self, item, parent_info=False):
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
item_id = item['Id']
2016-04-06 18:12:42 +00:00
artworks = item['ImageTags']
2016-06-16 05:43:36 +00:00
backdrops = item.get('BackdropImageTags', [])
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
max_height = 10000
max_width = 10000
custom_query = ""
2016-04-06 18:12:42 +00:00
2016-06-16 05:43:36 +00:00
if settings('compressArt') == "true":
2016-09-10 11:15:58 +00:00
custom_query = "&Quality=90"
2016-04-06 18:12:42 +00:00
2016-06-16 05:43:36 +00:00
if settings('enableCoverArt') == "false":
2016-09-10 11:15:58 +00:00
custom_query += "&EnableImageEnhancers=false"
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
all_artwork = {
2016-04-06 18:12:42 +00:00
'Primary': "",
'Art': "",
'Banner': "",
'Logo': "",
'Thumb': "",
'Disc': "",
'Backdrop': []
}
2016-09-20 06:58:25 +00:00
def get_backdrops(item_id, backdrops):
2016-09-10 11:15:58 +00:00
for index, tag in enumerate(backdrops):
artwork = ("%s/emby/Items/%s/Images/Backdrop/%s?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (self.server, item_id, index, max_width, max_height,
tag, custom_query))
all_artwork['Backdrop'].append(artwork)
2016-09-20 06:58:25 +00:00
def get_artwork(item_id, type_, tag):
2016-09-10 11:15:58 +00:00
artwork = ("%s/emby/Items/%s/Images/%s/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (self.server, item_id, type_, max_width, max_height, tag, custom_query))
all_artwork[type_] = artwork
2016-04-06 18:12:42 +00:00
# Process backdrops
2016-09-20 06:58:25 +00:00
get_backdrops(item_id, backdrops)
2016-04-06 18:12:42 +00:00
# Process the rest of the artwork
2016-09-10 11:15:58 +00:00
for artwork in artworks:
2016-04-06 18:12:42 +00:00
# Filter backcover
2016-09-10 11:15:58 +00:00
if artwork != "BoxRear":
2016-09-20 06:58:25 +00:00
get_artwork(item_id, artwork, artworks[artwork])
2016-04-06 18:12:42 +00:00
# Process parent items if the main item is missing artwork
2016-09-10 11:15:58 +00:00
if parent_info:
2016-04-06 18:12:42 +00:00
# Process parent backdrops
2016-09-10 11:15:58 +00:00
if not all_artwork['Backdrop']:
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
if 'ParentBackdropItemId' in item:
# If there is a parent_id, go through the parent backdrop list
2016-09-20 06:58:25 +00:00
get_backdrops(item['ParentBackdropItemId'], item['ParentBackdropImageTags'])
2016-04-06 18:12:42 +00:00
# Process the rest of the artwork
2016-09-10 11:15:58 +00:00
for parent_artwork in ('Logo', 'Art', 'Thumb'):
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
if not all_artwork[parent_artwork]:
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
if 'Parent%sItemId' % parent_artwork in item:
2016-09-20 06:58:25 +00:00
get_artwork(item['Parent%sItemId' % parent_artwork], parent_artwork,
item['Parent%sImageTag' % parent_artwork])
2016-04-06 18:12:42 +00:00
# Parent album works a bit differently
2016-09-10 11:15:58 +00:00
if not all_artwork['Primary']:
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
if 'AlbumId' in item and 'AlbumPrimaryImageTag' in item:
2016-09-20 06:58:25 +00:00
get_artwork(item['AlbumId'], 'Primary', item['AlbumPrimaryImageTag'])
2016-04-06 18:12:42 +00:00
2016-09-10 11:15:58 +00:00
return all_artwork