Hide keyboard shortcuts

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> 

3 

4This file is part of toktokkie. 

5 

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. 

10 

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. 

15 

16You should have received a copy of the GNU General Public License 

17along with toktokkie. If not, see <http://www.gnu.org/licenses/>. 

18LICENSE""" 

19 

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 

33 

34 

35class AlbumArtFetchCommand(Command): 

36 """ 

37 Class that encapsulates behaviour of the album-art-fetch command 

38 """ 

39 

40 @classmethod 

41 def name(cls) -> str: 

42 """ 

43 :return: The command name 

44 """ 

45 return "album-art-fetch" 

46 

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" 

53 

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) 

62 

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: 

76 

77 theme_song = theme_songs.get(album.name) 

78 

79 self.logger.info("Fetching cover art for {}" 

80 .format(album.name)) 

81 

82 album_icon_file = os.path.join( 

83 metadata.icon_directory, 

84 album.name + ".png" 

85 ) 

86 

87 if os.path.isfile(album_icon_file): 

88 self.logger.info("Album art already exists, skipping") 

89 continue 

90 

91 musicbrainz_ids = album.ids[IdType.MUSICBRAINZ_RELEASE] 

92 

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 

105 

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 

111 

112 self.download_cover_file(cover_urls, album_icon_file) 

113 

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" 

123 

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 

136 

137 if img is None: 

138 self.logger.warning("Couldn't download cover file {}".format(dest)) 

139 return 

140 

141 with open(tmp_file, "wb") as f: 

142 f.write(img.content) 

143 

144 image = Image.open(tmp_file) 

145 x, y = image.size 

146 new_y = 512 

147 new_x = int(new_y * x / y) 

148 

149 image = image.resize((new_x, new_y), Image.ANTIALIAS) 

150 

151 x, y = image.size 

152 size = 512 

153 

154 new = Image.new("RGBA", (512, 512), (0, 0, 0, 0)) 

155 new.paste(image, (int((size - x) / 2), int((size - y) / 2))) 

156 

157 with open(dest, "wb") as f: 

158 new.save(f) 

159 

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) 

169 

170 cover_page = BeautifulSoup( 

171 requests.get(cover_page_url).text, 

172 "html.parser" 

173 ) 

174 

175 urls = [] 

176 urlmap = { 

177 "original": [], 

178 "250px": [], 

179 "500px": [] 

180 } # type: Dict[str, List[str]] 

181 

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"]) 

187 

188 for category in ["original", "500px", "250px"]: 

189 for link in urlmap[category]: 

190 urls.append("https:" + link) 

191 

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 

199 

200 return urls 

201 

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 """ 

209 

210 client = GraphQlClient("https://graphql.anilist.co") 

211 

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"] 

223 

224 return [ 

225 cover_image.replace("medium", "large"), 

226 cover_image.replace("large", "medium"), 

227 cover_image.replace("large", "small").replace("medium", "small") 

228 ]