Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""LICENSE
2Copyright 2015 Hermann Krumrey <hermann@krumreyh.com>
4This file is part of toktokkie.
6toktokkie is free software: you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation, either version 3 of the License, or
9(at your option) any later version.
11toktokkie is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
16You should have received a copy of the GNU General Public License
17along with toktokkie. If not, see <http://www.gnu.org/licenses/>.
18LICENSE"""
20import os
21import requests
22import argparse
23from PIL import Image
24from bs4 import BeautifulSoup
25from typing import Dict, List
26from toktokkie.Directory import Directory
27from toktokkie.commands.Command import Command
28from toktokkie.enums import IdType
29from toktokkie.enums import MediaType
30from toktokkie.metadata.music.Music import Music
31from toktokkie.metadata.music.components.MusicThemeSong import MusicThemeSong
32from puffotter.graphql import GraphQlClient
35class AlbumArtFetchCommand(Command):
36 """
37 Class that encapsulates behaviour of the album-art-fetch command
38 """
40 @classmethod
41 def name(cls) -> str:
42 """
43 :return: The command name
44 """
45 return "album-art-fetch"
47 @classmethod
48 def help(cls) -> str:
49 """
50 :return: The help message for the command
51 """
52 return "Loads music album art based on stored IDs"
54 @classmethod
55 def prepare_parser(cls, parser: argparse.ArgumentParser):
56 """
57 Prepares an argumentparser for this command
58 :param parser: The parser to prepare
59 :return: None
60 """
61 cls.add_directories_arg(parser)
63 def execute(self):
64 """
65 Executes the commands
66 :return: None
67 """
68 for directory in Directory.load_directories(
69 self.args.directories, restrictions=[MediaType.MUSIC_ARTIST]
70 ):
71 metadata = directory.metadata # type: Music
72 theme_songs = {
73 x.name: x for x in metadata.theme_songs
74 } # type: Dict[str, MusicThemeSong]
75 for album in metadata.albums:
77 theme_song = theme_songs.get(album.name)
79 self.logger.info("Fetching cover art for {}"
80 .format(album.name))
82 album_icon_file = os.path.join(
83 metadata.icon_directory,
84 album.name + ".png"
85 )
87 if os.path.isfile(album_icon_file):
88 self.logger.info("Album art already exists, skipping")
89 continue
91 musicbrainz_ids = album.ids[IdType.MUSICBRAINZ_RELEASE]
93 if len(musicbrainz_ids) >= 1:
94 self.logger.debug("Using musicbrainz IDs")
95 cover_urls = self.load_musicbrainz_cover_url(
96 musicbrainz_ids[0]
97 )
98 elif theme_song is not None:
99 self.logger.debug("Using anilist IDs")
100 anilist_ids = theme_song.series_ids[IdType.ANILIST]
101 if len(anilist_ids) < 1:
102 self.logger.warning("{} has no anilist ID, skipping"
103 .format(theme_song.name))
104 continue
106 cover_urls = self.load_anilist_cover_url(anilist_ids[0])
107 else:
108 self.logger.warning("Couldn't find album art for {}"
109 .format(album.name))
110 continue
112 self.download_cover_file(cover_urls, album_icon_file)
114 def download_cover_file(self, urls: List[str], dest: str):
115 """
116 Downloads a cover file, then trims it correctly and/or converts
117 it to PNG
118 :param urls: The URLs to try
119 :param dest: The destination file location
120 :return: None
121 """
122 tmp_file = "/tmp/coverimage-temp"
124 img = None
125 while len(urls) > 0:
126 url = urls.pop(0)
127 self.logger.info("Trying to download {}".format(url))
128 img = requests.get(
129 url,
130 headers={"User-Agent": "Mozilla/5.0"}
131 )
132 if img.status_code < 300:
133 break
134 else:
135 img = None
137 if img is None:
138 self.logger.warning("Couldn't download cover file {}".format(dest))
139 return
141 with open(tmp_file, "wb") as f:
142 f.write(img.content)
144 image = Image.open(tmp_file)
145 x, y = image.size
146 new_y = 512
147 new_x = int(new_y * x / y)
149 image = image.resize((new_x, new_y), Image.ANTIALIAS)
151 x, y = image.size
152 size = 512
154 new = Image.new("RGBA", (512, 512), (0, 0, 0, 0))
155 new.paste(image, (int((size - x) / 2), int((size - y) / 2)))
157 with open(dest, "wb") as f:
158 new.save(f)
160 # noinspection PyMethodMayBeStatic
161 def load_musicbrainz_cover_url(self, musicbrainz_id: str) -> List[str]:
162 """
163 Loads cover image URls using musicbrainz release IDs
164 :param musicbrainz_id: The musicbrainz release ID to use
165 :return: The musicbrainz cover image URLs
166 """
167 cover_page_url = "https://musicbrainz.org/release/{}/cover-art"\
168 .format(musicbrainz_id)
170 cover_page = BeautifulSoup(
171 requests.get(cover_page_url).text,
172 "html.parser"
173 )
175 urls = []
176 urlmap = {
177 "original": [],
178 "250px": [],
179 "500px": []
180 } # type: Dict[str, List[str]]
182 for art in cover_page.select(".artwork-cont"):
183 for a in art.find_all("a"):
184 category = a.text.strip().lower()
185 if category in ["original", "250px", "500px"]:
186 urlmap[category].append(a["href"])
188 for category in ["original", "500px", "250px"]:
189 for link in urlmap[category]:
190 urls.append("https:" + link)
192 try:
193 displayed = cover_page.select(".cover-art")[0].find("img")["src"]
194 if displayed.startswith("/"):
195 displayed = "https:" + displayed
196 urls.append(displayed)
197 except (IndexError, TypeError):
198 pass
200 return urls
202 # noinspection PyMethodMayBeStatic
203 def load_anilist_cover_url(self, anilist_id: str) -> List[str]:
204 """
205 Loads cover image URLs using anilist IDs
206 :param anilist_id: The anilist ID to use
207 :return: The cover images that were found
208 """
210 client = GraphQlClient("https://graphql.anilist.co")
212 query = """
213 query ($id: Int) {
214 Media (id: $id) {
215 coverImage {
216 large
217 }
218 }
219 }
220 """
221 data = client.query(query, {"id": int(anilist_id)})["data"]
222 cover_image = data["Media"]["coverImage"]["large"]
224 return [
225 cover_image.replace("medium", "large"),
226 cover_image.replace("large", "medium"),
227 cover_image.replace("large", "small").replace("medium", "small")
228 ]