|
| 1 | +""" |
| 2 | + by oPromessa, 2024 |
| 3 | +
|
| 4 | + Example use of vsMetaFileEncoder. |
| 5 | + * Generates vsmeta from seraching on IMDb. |
| 6 | + * Also checks vsmeta file and shows its contents. |
| 7 | +
|
| 8 | + Makes use of modules: click, requests, idmbmovies |
| 9 | +
|
| 10 | + To use, install required modules with pip: |
| 11 | + pip install click |
| 12 | + pip install requests |
| 13 | + pip install imdbmovies |
| 14 | +""" |
| 15 | +import os |
| 16 | +import re |
| 17 | + |
| 18 | +from datetime import date, datetime |
| 19 | +import textwrap |
| 20 | + |
| 21 | +import click |
| 22 | +import requests |
| 23 | + |
| 24 | +from imdbmovies import IMDB |
| 25 | + |
| 26 | +from vsmetaCodec.vsmetaEncoder import VsMetaMovieEncoder |
| 27 | +from vsmetaCodec.vsmetaDecoder import VsMetaDecoder |
| 28 | +from vsmetaCodec.vsmetaInfo import VsMetaImageInfo |
| 29 | + |
| 30 | + |
| 31 | +def write_vsmeta_file(filename: str, content: bytes): |
| 32 | + """ Writes to file in binary mode. Used to write .vsmeta files. |
| 33 | + """ |
| 34 | + with open(filename, 'wb') as write_file: |
| 35 | + write_file.write(content) |
| 36 | + write_file.close() |
| 37 | + |
| 38 | + |
| 39 | +def read_vsmeta_file(filename: str) -> bytes: |
| 40 | + """ Reads from file in binary mode. Used to read .vsmeta files. |
| 41 | + """ |
| 42 | + with open(filename, 'rb') as read_file: |
| 43 | + file_content = read_file.read() |
| 44 | + read_file.close() |
| 45 | + return file_content |
| 46 | + |
| 47 | + |
| 48 | +def lookfor_imdb(movie_title, year=None, tv=False): |
| 49 | + """ Returns movie_info of first movie from year returned by search in IMDb. |
| 50 | + """ |
| 51 | + |
| 52 | + imdb = IMDB() |
| 53 | + results = imdb.search(movie_title, year=year, tv=tv) |
| 54 | + |
| 55 | + # Filter only movie type entries |
| 56 | + movie_results = [result for result in results["results"] |
| 57 | + if result["type"] == "movie"] |
| 58 | + |
| 59 | + print( |
| 60 | + f"Found: [{len(movie_results)}] entries for " |
| 61 | + f"Title: [{movie_title}] Year: [{year}]" |
| 62 | + ) |
| 63 | + |
| 64 | + for cnt, mv in enumerate(movie_results): |
| 65 | + print( |
| 66 | + f"\tEntry: [{cnt}] Name: [{click.style(mv['name'], fg='yellow')}] " |
| 67 | + f"Id: [{mv['id']}] Type: [{mv['type']}]") |
| 68 | + |
| 69 | + if movie_results: |
| 70 | + movie_info = imdb.get_by_id(movie_results[0]['id']) |
| 71 | + return movie_results[0]['id'], movie_info |
| 72 | + |
| 73 | + return None, None |
| 74 | + |
| 75 | + |
| 76 | +def download_poster(url, filename): |
| 77 | + """ Downloads Image from URL into a JPG file. |
| 78 | + """ |
| 79 | + http_timeout = 15 |
| 80 | + |
| 81 | + response = requests.get(url, timeout=http_timeout) |
| 82 | + if response.status_code == 200: |
| 83 | + with open(filename, 'wb') as f: |
| 84 | + f.write(response.content) |
| 85 | + |
| 86 | + |
| 87 | +def find_metadata(title, year, filename, verbose): |
| 88 | + """Search for a movie/Year metada on IMDb. |
| 89 | +
|
| 90 | + If found, downloads to a local .JPG file the poster |
| 91 | + """ |
| 92 | + print( |
| 93 | + f"-------------- : Processing title [{click.style(title, fg='green')}] " |
| 94 | + f"year [{year}] filename [{filename}]") |
| 95 | + |
| 96 | + vsmeta_filename = None |
| 97 | + |
| 98 | + year = None if year is None else int(year) |
| 99 | + |
| 100 | + # Search IMDB for movie information |
| 101 | + movie_id, movie_info = lookfor_imdb(title, year=year) |
| 102 | + |
| 103 | + if movie_id and movie_info: |
| 104 | + # Download poster |
| 105 | + poster_url = movie_info['poster'] |
| 106 | + poster_filename = f'{title.replace(" ", "_")}_poster.jpg' |
| 107 | + download_poster(poster_url, poster_filename) |
| 108 | + |
| 109 | + # Map IMDB fields to VSMETA |
| 110 | + # and Encode VSMETA |
| 111 | + vsmeta_filename = filename + ".vsmeta" |
| 112 | + map_to_vsmeta(movie_id, movie_info, poster_filename, |
| 113 | + vsmeta_filename, verbose) |
| 114 | + else: |
| 115 | + print(f"No information found for '{click.style(title, fg='red')}'") |
| 116 | + |
| 117 | + print( |
| 118 | + f"\tProcessed title [{click.style(title, fg='green')}] year [{year}] " |
| 119 | + f"vsmeta [{vsmeta_filename}]") |
| 120 | + |
| 121 | + return vsmeta_filename |
| 122 | + |
| 123 | + |
| 124 | +def map_to_vsmeta(imdb_id, imdb_info, poster_file, vsmeta_filename, verbose): |
| 125 | + """Encodes a .VSMETA file based on imdb_info and poster_file """ |
| 126 | + |
| 127 | + # vsmetaMovieEncoder |
| 128 | + vsmeta_writer = VsMetaMovieEncoder() |
| 129 | + |
| 130 | + # Build up vsmeta info |
| 131 | + info = vsmeta_writer.info |
| 132 | + |
| 133 | + # Title |
| 134 | + info.showTitle = imdb_info['name'] |
| 135 | + info.showTitle2 = imdb_info['name'] |
| 136 | + # Tag line |
| 137 | + info.episodeTitle = f"{imdb_info['name']}" |
| 138 | + |
| 139 | + # Publishing Date |
| 140 | + info.setEpisodeDate(date( |
| 141 | + int(imdb_info['datePublished'][:4]), |
| 142 | + int(imdb_info['datePublished'][5:7]), |
| 143 | + int(imdb_info['datePublished'][8:]))) |
| 144 | + |
| 145 | + # Set to 0 for Movies: season and episode |
| 146 | + info.season = 0 |
| 147 | + info.episode = 0 |
| 148 | + |
| 149 | + # Not used. Set to 1900-01-01 |
| 150 | + info.tvshowReleaseDate = date(1900, 1, 1) |
| 151 | + |
| 152 | + # Locked = False |
| 153 | + info.episodeLocked = False |
| 154 | + |
| 155 | + info.timestamp = int(datetime.now().timestamp()) |
| 156 | + |
| 157 | + # Classification |
| 158 | + # A classification of None would crash the reading of .vsmeta file with error |
| 159 | + info.classification = "" if imdb_info['contentRating'] is None else imdb_info['contentRating'] |
| 160 | + |
| 161 | + # Rating |
| 162 | + info.rating = imdb_info['rating']['ratingValue'] |
| 163 | + |
| 164 | + # Summary |
| 165 | + info.chapterSummary = imdb_info['description'] |
| 166 | + |
| 167 | + # Cast |
| 168 | + info.list.cast = [] |
| 169 | + for actor in imdb_info['actor']: |
| 170 | + info.list.cast.append(actor['name']) |
| 171 | + |
| 172 | + # Director |
| 173 | + info.list.director = [] |
| 174 | + for director in imdb_info['director']: |
| 175 | + info.list.director.append(director['name']) |
| 176 | + |
| 177 | + # Writer |
| 178 | + info.list.writer = [] |
| 179 | + for creator in imdb_info['creator']: |
| 180 | + info.list.writer.append(creator['name']) |
| 181 | + |
| 182 | + # Genre |
| 183 | + info.list.genre = imdb_info['genre'] |
| 184 | + |
| 185 | + # Read JPG images for Poster and Background |
| 186 | + with open(poster_file, "rb") as image: |
| 187 | + f = image.read() |
| 188 | + |
| 189 | + # Poster (of Movie) |
| 190 | + episode_img = VsMetaImageInfo() |
| 191 | + episode_img.image = f |
| 192 | + info.episodeImageInfo.append(episode_img) |
| 193 | + |
| 194 | + # Background (of Movie) |
| 195 | + # Use Posters file for Backdrop also |
| 196 | + info.backdropImageInfo.image = f |
| 197 | + |
| 198 | + # Not used. Set to VsImageIfnfo() |
| 199 | + info.posterImageInfo = episode_img |
| 200 | + |
| 201 | + if verbose: |
| 202 | + print("\t---------------: ---------------") |
| 203 | + print(f"\tIMDB id : {imdb_id}") |
| 204 | + print(f"\tTitle : {info.showTitle}") |
| 205 | + print(f"\tTitle2 : {info.showTitle2}") |
| 206 | + print(f"\tEpisode title : {info.episodeTitle}") |
| 207 | + print(f"\tEpisode year : {info.year}") |
| 208 | + print(f"\tEpisode date : {info.episodeReleaseDate}") |
| 209 | + print(f"\tEpisode locked : {info.episodeLocked}") |
| 210 | + print(f"\tTimeStamp : {info.timestamp}") |
| 211 | + print(f"\tClassification : {info.classification}") |
| 212 | + print(f"\tRating : {info.rating:1.1f}") |
| 213 | + wrap_text = "\n\t ".join( |
| 214 | + textwrap.wrap(info.chapterSummary, 150)) |
| 215 | + print(f"\tSummary : {wrap_text}") |
| 216 | + print( |
| 217 | + f"\tCast : {''.join([f'{name}, ' for name in info.list.cast])}") |
| 218 | + print( |
| 219 | + f"\tDirector : {''.join([f'{name}, ' for name in info.list.director])}") |
| 220 | + print( |
| 221 | + f"\tWriter : {''.join([f'{name}, ' for name in info.list.writer])}") |
| 222 | + print( |
| 223 | + f"\tGenre : {''.join([f'{name}, ' for name in info.list.genre])}") |
| 224 | + print("\t---------------: ---------------") |
| 225 | + |
| 226 | + write_vsmeta_file(vsmeta_filename, vsmeta_writer.encode(info)) |
| 227 | + |
| 228 | + return True |
| 229 | + |
| 230 | + |
| 231 | +def find_files(root_dir, valid_ext=(".mp4", ".mkv", ".avi", ".mpg")): |
| 232 | + """ Returns files with extension in valid_ext list |
| 233 | + """ |
| 234 | + |
| 235 | + for root, _, files in os.walk(root_dir): |
| 236 | + for file in files: |
| 237 | + if any(file.casefold().endswith(ext) for ext in valid_ext) and \ |
| 238 | + not os.path.isdir(os.path.join(root, file)): |
| 239 | + yield os.path.join(root, file) |
| 240 | + |
| 241 | + |
| 242 | +def extract_info(file_path): |
| 243 | + """ Convert file_path into dirname and from basename extract |
| 244 | + movie_tile and year. Expecting filename format 'movie title name (1999)' |
| 245 | + """ |
| 246 | + dirname = os.path.dirname(file_path) |
| 247 | + basename = os.path.basename(file_path) |
| 248 | + |
| 249 | + # filtered_value = re.search(r'\D*(\d{4})', basename) |
| 250 | + filtered_value = re.search(r'^(.*?)(\d{4})(.*)$', basename) |
| 251 | + filtered_title = None |
| 252 | + filtered_year = None |
| 253 | + if filtered_value: |
| 254 | + filtered_title = filtered_value.group(1) |
| 255 | + filtered_year = filtered_value.group(2) |
| 256 | + else: |
| 257 | + filtered_title = basename |
| 258 | + |
| 259 | + filtered_title = filtered_title.replace('.', ' ').strip() |
| 260 | + |
| 261 | + return dirname, basename, filtered_title, filtered_year |
| 262 | + |
| 263 | + |
| 264 | +def check_file(file_path): |
| 265 | + """Read .vsmeta file and print it's contents. |
| 266 | +
|
| 267 | + Images within .vsmeta are saved as image_back_drop.jpg and image_poster_NN.jpg |
| 268 | + When checking multiple files, these files are overwritten. |
| 269 | + """ |
| 270 | + |
| 271 | + vsmeta_bytes = read_vsmeta_file(file_path) |
| 272 | + reader = VsMetaDecoder() |
| 273 | + reader.decode(vsmeta_bytes) |
| 274 | + |
| 275 | + reader.info.printInfo('.', prefix=os.path.basename(file_path)) |
| 276 | + |
| 277 | + |
| 278 | +@click.command() |
| 279 | +@click.option('--search', |
| 280 | + type=click.Path(exists=True, |
| 281 | + file_okay=False, |
| 282 | + dir_okay=True, |
| 283 | + resolve_path=True), |
| 284 | + help="Folder to recursively search for media files to be processed into .vsmeta.") |
| 285 | +@click.option("--check", |
| 286 | + type=click.Path(exists=True, |
| 287 | + file_okay=True, |
| 288 | + dir_okay=True, |
| 289 | + resolve_path=True), |
| 290 | + help="Check .vsmeta files. Show info. " |
| 291 | + "Exclusive with --search option.") |
| 292 | +@click.option('-v', '--verbose', is_flag=True, |
| 293 | + help="Shows info found on IMDB.") |
| 294 | +def main(search, check, verbose): |
| 295 | + """Searches on a folder for Movie Titles and generates .vsmeta files. |
| 296 | + You can then copy them over to your Library. |
| 297 | + """ |
| 298 | + |
| 299 | + if not (check or search) or (check and search): |
| 300 | + raise SystemExit( |
| 301 | + "Must specify at least one (and exclusively) option " |
| 302 | + "--search or --check. Use --help for additional help.") |
| 303 | + |
| 304 | + if check: |
| 305 | + if os.path.isfile(check) and check.endswith(".vsmeta"): |
| 306 | + print(f"-------------- : Checking file [{check}]") |
| 307 | + check_file(check) |
| 308 | + elif os.path.isdir(check): |
| 309 | + for found_file in find_files(check, valid_ext=('.vsmeta', )): |
| 310 | + print(f"-------------- : Checking file [{check}]") |
| 311 | + check_file(found_file) |
| 312 | + else: |
| 313 | + raise print( |
| 314 | + "Invalid check path or file name. " |
| 315 | + "sPlease provide a valid directory or .vsmeta file.") |
| 316 | + |
| 317 | + if search: |
| 318 | + print(f"Processing folder: [{search}].") |
| 319 | + |
| 320 | + # Iterate over the matching files |
| 321 | + for found_file in find_files(search): |
| 322 | + # print(f"Found file: [{found_file}]") |
| 323 | + _, basename, title, year = extract_info(found_file) |
| 324 | + find_metadata(title, year, basename, verbose) |
| 325 | + |
| 326 | + |
| 327 | +if __name__ == "__main__": |
| 328 | + main() |
0 commit comments