MediaProcessor.pyView |
---|
| 1 … | + |
| 2 … | + |
| 3 … | + |
| 4 … | + |
| 5 … | + |
| 6 … | + |
| 7 … | + |
| 8 … | + |
| 9 … | + |
| 10 … | + |
| 11 … | + |
| 12 … | + |
| 13 … | + |
| 14 … | + |
| 15 … | + |
| 16 … | + |
| 17 … | + |
| 18 … | + |
| 19 … | + |
| 20 … | + |
| 21 … | + |
| 22 … | + |
| 23 … | + |
| 24 … | + |
| 25 … | + |
| 26 … | + |
| 27 … | + |
| 28 … | + |
| 29 … | +from __future__ import unicode_literals |
| 30 … | +from email.message import EmailMessage |
| 31 … | +from mpServerDefinitions import * |
| 32 … | +from tinytag import TinyTag |
| 33 … | +from yt_dlp import utils |
| 34 … | +from datetime import * |
| 35 … | +import Exceptions |
| 36 … | +import subprocess |
| 37 … | +import threading |
| 38 … | +import smtplib |
| 39 … | +import sqlite3 |
| 40 … | +import yt_dlp |
| 41 … | +import time |
| 42 … | +import json |
| 43 … | +import ssl |
| 44 … | +import sys |
| 45 … | +import os |
| 46 … | +import re |
| 47 … | + |
| 48 … | + |
| 49 … | +class MediaProcessor: |
| 50 … | + |
| 51 … | + def __init__(self): |
| 52 … | + self.SQLrows2Add = [] |
| 53 … | + self.ErrorList = [] |
| 54 … | + self.Config = {} |
| 55 … | + |
| 56 … | + """ config file template, JSON format. Use single quotes only, null not None: |
| 57 … | + self.Config { |
| 58 … | + "Comment": [ "Unreferenced - for comments inside config file", |
| 59 … | + "It is difficult sometimes to find the video link for Brightcove videos.", |
| 60 … | + "This is the method that I use with Firefox. You will need 2 things:", |
| 61 … | + "a) AccountID", |
| 62 … | + "b) VideoID", |
| 63 … | + "Get these by right-clicking on the video and select Player Information.", |
| 64 … | + "Use ctrl-C to copy the info, and plug them into this template:", |
| 65 … | + "http://players.brightcove.net/<AccountID>/default_default/index.html?videoId=<VideoID>", |
| 66 … | + "Works with Firefox 68.8, 75.0 and probably others as of May 12, 2020" |
| 67 … | + ], |
| 68 … | + |
| 69 … | + "DLbase": "dir", Folder for all downloaded files organized by grupe |
| 70 … | + "DLeLoselfg":"file", File for exceptions / errors during downloads |
| 71 … | + "DLarch": "file", This tracks downloads to skip those already done |
| 72 … | + "DLmeta": "file", The metadata definition is now split into this file |
| 73 … | + |
| 74 … | + "DLOpts": { Name / value pairs for youtube-dl options |
| 75 … | + "optName1": "value1", NOT always the same as cmd line opts |
| 76 … | + ... |
| 77 … | + }, |
| 78 … | + |
| 79 … | + "Grupes": { |
| 80 … | + "gName1": { Group name containing video selection criteria |
| 81 … | + "Active": true, Enable or disable downloads for this grupe |
| 82 … | + "Duration": 0, Min size of video in seconds; for no limits use 0 |
| 83 … | + "Quota": null, Limits size of grupe's DL folder to N files |
| 84 … | + "Start": null, Earliest upload date string or (YYYYMMDD) or null |
| 85 … | + "End": null, Latest upload date or null. 1, 2 or neither OK |
| 86 … | + "Stop": null, Stop downloading from playlist after this many DLs |
| 87 … | + "url1", |
| 88 … | + "url2", |
| 89 … | + ... |
| 90 … | + ] |
| 91 … | + }, |
| 92 … | + ... Additional grupes |
| 93 … | + }, |
| 94 … | + |
| 95 … | + "MetaColumns": [ Contains the list of database fields for metadata for |
| 96 … | + ... the video downloaded along with it in JSON format. |
| 97 … | + This section is loaded from a separate file which is |
| 98 … | + specified by the DLbase and DLmeta key defined above. |
| 99 … | + ] |
| 100 … | + } |
| 101 … | + """ |
| 102 … | + |
| 103 … | + def usage(self): |
| 104 … | + cmd = sys.argv[0] |
| 105 … | + str = "\nUses youtube-dl to download videos and add them to IPFS and track\n" |
| 106 … | + str += "the results in a SQLite database.\n\n" |
| 107 … | + str += "Usage: " + cmd + " [-h] | <-c config> <-d sqlite> [-g grupe]\n" |
| 108 … | + str += "-h or no args print this help message.\n\n" |
| 109 … | + str += "-c is a JSON formated config file that specifies the target groups,\n" |
| 110 … | + str += "their URL(s), downloader options, the base or top level folder for\n" |
| 111 … | + str += "the groups of files downloaded and the list of metadata columns.\n" |
| 112 … | + str += "-d is the SQLite filename (it is created if it doesn't exist).\n\n" |
| 113 … | + str += "-g ignore all but the grupes in config except the one name after -g.\n\n" |
| 114 … | + print(str) |
| 115 … | + exit(0) |
| 116 … | + |
| 117 … | + |
| 118 … | + |
| 119 … | + def decodeException(self, e, all=False): |
| 120 … | + trace = [] |
| 121 … | + stk = 0 |
| 122 … | + tb = e.__traceback__ |
| 123 … | + while tb is not None: |
| 124 … | + stk += 1 |
| 125 … | + trace.append({ |
| 126 … | + "stk": stk, |
| 127 … | + "filename": tb.tb_frame.f_code.co_filename, |
| 128 … | + "function": tb.tb_frame.f_code.co_name, |
| 129 … | + "lineno": tb.tb_lineno |
| 130 … | + }) |
| 131 … | + tb = tb.tb_next |
| 132 … | + if not all: |
| 133 … | + trace = trace[-1] |
| 134 … | + del trace["stk"] |
| 135 … | + return { |
| 136 … | + 'type': type(e).__name__, |
| 137 … | + 'message': str(e), |
| 138 … | + 'trace': trace |
| 139 … | + } |
| 140 … | + |
| 141 … | + |
| 142 … | + def flatten_json(self, nested_json): |
| 143 … | + out = {} |
| 144 … | + def flatten(x, name=''): |
| 145 … | + if type(x) is dict: |
| 146 … | + for a in x: |
| 147 … | + flatten(x[a], name + a + '_') |
| 148 … | + elif type(x) is list: |
| 149 … | + i = 0 |
| 150 … | + for a in x: |
| 151 … | + flatten(a, name + str(i) + '_') |
| 152 … | + i += 1 |
| 153 … | + else: |
| 154 … | + out[name[:-1]] = x |
| 155 … | + |
| 156 … | + flatten(nested_json) |
| 157 … | + return out |
| 158 … | + |
| 159 … | + |
| 160 … | + |
| 161 … | + |
| 162 … | + def openSQLiteDB(self, dbFile): |
| 163 … | + newDatabase = not os.path.exists(dbFile) |
| 164 … | + columns = self.Config['MetaColumns'] |
| 165 … | + conn = sqlite3.connect(dbFile) |
| 166 … | + if newDatabase: |
| 167 … | + sql = '''create table if not exists IPFS_INFO ( |
| 168 … | + "sqlts" TIMESTAMP NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now', 'localtime')), |
| 169 … | + "pky" INTEGER PRIMARY KEY AUTOINCREMENT, |
| 170 … | + "db_hash" TEXT, |
| 171 … | + "dl_good" INTEGER DEFAULT 0, |
| 172 … | + "dl_errs" INTEGER DEFAULT 0);''' |
| 173 … | + conn.execute(sql) |
| 174 … | + |
| 175 … | + sql = '''create table if not exists IPFS_HASH_INDEX ( |
| 176 … | + "sqlts" TIMESTAMP NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now', 'localtime')), |
| 177 … | + "pky" INTEGER PRIMARY KEY AUTOINCREMENT, |
| 178 … | + "g_idx" TEXT, |
| 179 … | + "grupe" TEXT, |
| 180 … | + "vhash" TEXT, |
| 181 … | + "vsize" TEXT, |
| 182 … | + "mhash" TEXT''' |
| 183 … | + for c in columns: |
| 184 … | + sql += ',\n\t"' + c + '" TEXT' |
| 185 … | + sql += ')' |
| 186 … | + conn.execute(sql) |
| 187 … | + return conn |
| 188 … | + |
| 189 … | + |
| 190 … | + def pin2IPFS(self, hash): |
| 191 … | + cmd = ["ipfs", "pin", "add", hash] |
| 192 … | + out = subprocess.run(cmd, stderr=subprocess.DEVNULL, |
| 193 … | + stdout=subprocess.PIPE).stdout.decode('utf-8') |
| 194 … | + if out.startswith("pinned"): return True |
| 195 … | + else: return False |
| 196 … | + |
| 197 … | + |
| 198 … | + |
| 199 … | + |
| 200 … | + def add2IPFS(self, file): |
| 201 … | + cmd = ["ipfs", "add", "-Q", file] |
| 202 … | + out = subprocess.run(cmd, stderr=subprocess.DEVNULL, |
| 203 … | + stdout=subprocess.PIPE).stdout.decode('utf-8') |
| 204 … | + if out.startswith("Qm") and len(out) == 47: |
| 205 … | + hash = out[0:46] |
| 206 … | + else: hash = "" |
| 207 … | + return hash |
| 208 … | + |
| 209 … | + |
| 210 … | + |
| 211 … | + |
| 212 … | + |
| 213 … | + |
| 214 … | + def publishDB(self, file): |
| 215 … | + newDBhash = self.add2IPFS(file) |
| 216 … | + if len(newDBhash) == 46: |
| 217 … | + if STATIC_DB_HASH: |
| 218 … | + cmd = ["ipfs", "name", "publish", "-Q", "-key=" + STATIC_DB_HASH, newDBhash] |
| 219 … | + cp = subprocess.run(cmd, capture_output=True, text=True) |
| 220 … | + if cp.returncode != 0: |
| 221 … | + print(f"Error publishing database to IPFS: {cp.stderr}", flush=True) |
| 222 … | + return newDBhash |
| 223 … | + |
| 224 … | + |
| 225 … | + |
| 226 … | + |
| 227 … | + |
| 228 … | + def updateRunInfo(self, sqlFile, dbHash, good, errs): |
| 229 … | + conn = self.openSQLiteDB(sqlFile) |
| 230 … | + conn.row_factory = sqlite3.Row |
| 231 … | + sql = '''INSERT INTO IPFS_INFO ("db_hash", "dl_good", "dl_errs") |
| 232 … | + VALUES (?,?,?);''' |
| 233 … | + |
| 234 … | + if conn is not None: |
| 235 … | + conn.execute(sql, (dbHash, good, errs)) |
| 236 … | + conn.commit() |
| 237 … | + conn.execute('VACUUM;') |
| 238 … | + conn.close() |
| 239 … | + |
| 240 … | + |
| 241 … | + |
| 242 … | + |
| 243 … | + |
| 244 … | + def filterUrls(self, conn, configUrls): |
| 245 … | + regx = re.compile(r'^.*?watch\?v=([^&]+)&*.*$', re.IGNORECASE) |
| 246 … | + cursor = conn.cursor() |
| 247 … | + filteredUrls = [] |
| 248 … | + for url in configUrls: |
| 249 … | + match = re.search(regx, url) |
| 250 … | + if match is not None: |
| 251 … | + id = match.group(1) |
| 252 … | + sql = "SELECT COUNT(*) FROM IPFS_HASH_INDEX WHERE id = ?;" |
| 253 … | + if cursor.execute(sql, [id]).fetchone()[0] == 0: |
| 254 … | + filteredUrls.append(url) |
| 255 … | + else: filteredUrls.append(url) |
| 256 … | + return filteredUrls |
| 257 … | + |
| 258 … | + |
| 259 … | + |
| 260 … | + |
| 261 … | + def updateGrupeIndex(self, conn, grupe): |
| 262 … | + cursor = conn.cursor() |
| 263 … | + |
| 264 … | + sql = 'SELECT "v=" || vhash || " " || "m=" || mhash' |
| 265 … | + sql += ' FROM IPFS_HASH_INDEX' |
| 266 … | + sql += ' WHERE grupe = ?' |
| 267 … | + idxFile = f"/tmp/{grupe}_idx.txt" |
| 268 … | + with open(idxFile, "w") as idx: |
| 269 … | + for row in cursor.execute(sql, (grupe,)): |
| 270 … | + if row[0]: idx.write(row[0] + '\n') |
| 271 … | + hash = self.add2IPFS(idxFile) |
| 272 … | + if len(hash) > 0: |
| 273 … | + sql = "UPDATE IPFS_HASH_INDEX set g_idx=? WHERE grupe=?" |
| 274 … | + cursor.execute(sql, (hash, grupe)) |
| 275 … | + conn.commit() |
| 276 … | + os.remove(idxFile) |
| 277 … | + return cursor.rowcount, hash |
| 278 … | + |
| 279 … | + |
| 280 … | + |
| 281 … | + |
| 282 … | + |
| 283 … | + def regenerateAllGrupeIndexes(self, conn): |
| 284 … | + cursor = conn.cursor() |
| 285 … | + sql = "sSELECT DISTINCT grupe FROM IPFS_HASH_INDEX" |
| 286 … | + for row in cursor.execute(sql): |
| 287 … | + (count, hash) = self.updateGrupeIndex(conn, row[0]) |
| 288 … | + print("Updated %d rows for grupe %s with grupe index hash %s" % |
| 289 … | + (count, row[0], hash), flush=True) |
| 290 … | + |
| 291 … | + |
| 292 … | + |
| 293 … | + |
| 294 … | + def addRow2db(self, conn, cols, gvmjList): |
| 295 … | + (grupe, vhash, vsize, mhash, jsn) = gvmjList |
| 296 … | + cursor = conn.cursor() |
| 297 … | + jsn["episode_number"] = mhash |
| 298 … | + jsn["season_number"] = vhash |
| 299 … | + values = [grupe, vhash, vsize, mhash] |
| 300 … | + sql = 'INSERT INTO IPFS_HASH_INDEX ("grupe", "vhash", "vsize", "mhash"' |
| 301 … | + |
| 302 … | + for col in cols: |
| 303 … | + sql += ',\n\t"' + col + '"' |
| 304 … | + |
| 305 … | + sql += ") VALUES (?,?,?,?" |
| 306 … | + for col in cols: |
| 307 … | + sql += ",?" |
| 308 … | + values.append(jsn[col]) |
| 309 … | + sql += "); " |
| 310 … | + cursor.execute(sql, values) |
| 311 … | + conn.commit() |
| 312 … | + return cursor.lastrowid |
| 313 … | + |
| 314 … | + |
| 315 … | + |
| 316 … | + |
| 317 … | + |
| 318 … | + |
| 319 … | + |
| 320 … | + |
| 321 … | + |
| 322 … | + |
| 323 … | + def addRow(self, conn, cols, gvmjList): |
| 324 … | + try: |
| 325 … | + row = self.addRow2db(conn, cols, gvmjList) |
| 326 … | + |
| 327 … | + |
| 328 … | + |
| 329 … | + |
| 330 … | + except (sqlite3.OperationalError, KeyError) as e: |
| 331 … | + newDictionary = {} |
| 332 … | + (grp, vhash, vsize, mhash, jsn) = gvmjList |
| 333 … | + for col in cols: |
| 334 … | + if col in jsn.keys(): |
| 335 … | + newDictionary[col] = jsn[col] |
| 336 … | + else: newDictionary[col] = "?" |
| 337 … | + |
| 338 … | + |
| 339 … | + row = self.addRow2db(conn, cols, (grp, vhash, vsize, mhash, newDictionary)) |
| 340 … | + return row |
| 341 … | + |
| 342 … | + |
| 343 … | + |
| 344 … | + |
| 345 … | + def processGrupeResults(self, conn, cols, items, grupe, eLog): |
| 346 … | + downloads = len(self.SQLrows2Add) |
| 347 … | + good = 0 |
| 348 … | + |
| 349 … | + if downloads > 0: |
| 350 … | + for dat in self.SQLrows2Add: |
| 351 … | + try: |
| 352 … | + self.addRow(conn, cols, dat) |
| 353 … | + good += 1 |
| 354 … | + |
| 355 … | + |
| 356 … | + except Exception as expn: |
| 357 … | + args = (dat[0], dat[1], dat[2], dat[3], dat[4], decodeException(expn)) |
| 358 … | + er = "SQL Error! Grupe=%s vHash=%s vSize=%s, mHash=%s\n\nJSON=%s\n%s" % args |
| 359 … | + er += "\nMetadata key/values used:\n" |
| 360 … | + json = dat[4] |
| 361 … | + for key in json: |
| 362 … | + er += "%32s = %s\n" % (key, json[key]) |
| 363 … | + self.ErrorList.append(er) |
| 364 … | + |
| 365 … | + self.updateGrupeIndex(conn, grupe) |
| 366 … | + |
| 367 … | + |
| 368 … | + failures = len(self.ErrorList) |
| 369 … | + if len(self.ErrorList) > 0: |
| 370 … | + eLog.write("PROCESSING ERRORS FOR GRUPE=%s:\n" % grupe) |
| 371 … | + for error in self.ErrorList: |
| 372 … | + eLog.write(error + '\n') |
| 373 … | + eLog.write("END OF ERRORS FOR %s\n\n" % grupe) |
| 374 … | + |
| 375 … | + args = (items, downloads, failures) |
| 376 … | + print("Processed=%d (Succeeded=%d, Failed=%d)" % args, flush=True) |
| 377 … | + return good, failures |
| 378 … | + |
| 379 … | + |
| 380 … | + def getSize(self, path): |
| 381 … | + totalSize = 0 |
| 382 … | + for f in os.listdir(path): |
| 383 … | + fp = os.path.join(path, f) |
| 384 … | + totalSize += os.path.getsize(fp) |
| 385 … | + return totalSize |
| 386 … | + |
| 387 … | + |
| 388 … | + |
| 389 … | + |
| 390 … | + |
| 391 … | + |
| 392 … | + |
| 393 … | + |
| 394 … | + |
| 395 … | + |
| 396 … | + |
| 397 … | + def pruneDir(self, quota, dir): |
| 398 … | + max = count = 0 |
| 399 … | + fList = [] |
| 400 … | + |
| 401 … | + if quota: |
| 402 … | + q = quota.split(' ') |
| 403 … | + if q[0].isdecimal(): |
| 404 … | + max = int(q[0]) |
| 405 … | + if max < 2: |
| 406 … | + err = "Invalid quota: " + dir |
| 407 … | + self.ErrorList.append(err) |
| 408 … | + return False |
| 409 … | + |
| 410 … | + for f in os.listdir(dir): |
| 411 … | + if f.endswith(EXT_LIST): |
| 412 … | + fList.append(dir + '/' + f) |
| 413 … | + count += 1 |
| 414 … | + if count < 2: return False |
| 415 … | + |
| 416 … | + old = min(fList, key=os.path.getctime) |
| 417 … | + |
| 418 … | + if len(q) > 1: size = 0 |
| 419 … | + else: size = self.getSize(dir) |
| 420 … | + if count > max or size > max: |
| 421 … | + rm = old.rsplit('.')[0] + ".*" |
| 422 … | + os.system("rm -rf %s" % rm) |
| 423 … | + return True |
| 424 … | + else: return False |
| 425 … | + |
| 426 … | + |
| 427 … | + def fixDuration(self, name, seconds=0): |
| 428 … | + duration = str(int(float(seconds))) |
| 429 … | + noDur = SEPARATOR + "NA" + SEPARATOR |
| 430 … | + newDur = SEPARATOR + duration + SEPARATOR |
| 431 … | + zeroDur = SEPARATOR + "0" + SEPARATOR |
| 432 … | + if noDur in name: |
| 433 … | + newName = name.replace(noDur, newDur) |
| 434 … | + elif zeroDur in name: |
| 435 … | + newName = name.replace(zeroDur, newDur) |
| 436 … | + else: |
| 437 … | + newName = name |
| 438 … | + if newName != name: os.rename(name, newName) |
| 439 … | + return newName |
| 440 … | + |
| 441 … | + |
| 442 … | + def parseFilename(self, file): |
| 443 … | + dir, base = file.rsplit('/', 1) |
| 444 … | + grp = dir.rsplit('/', 1)[1] |
| 445 … | + pb, ext = os.path.splitext(file) |
| 446 … | + mFile = pb + ".info.json" |
| 447 … | + vFile = file |
| 448 … | + return [dir, base, grp, pb, ext, vFile, mFile] |
| 449 … | + |
| 450 … | + |
| 451 … | + |
| 452 … | + |
| 453 … | + def processVideo(self, file): |
| 454 … | + vHash = mHash = jFlat = vSize = duration = None |
| 455 … | + |
| 456 … | + print(f"\nProcessing video file: {file}", flush=True) |
| 457 … | + p = self.parseFilename(file) |
| 458 … | + dir,base,grp,pb,ext,vFile,mFile = p |
| 459 … | + |
| 460 … | + |
| 461 … | + |
| 462 … | + quota = self.Config["Grupes"][grp]["Quota"] |
| 463 … | + pruned = False |
| 464 … | + while self.pruneDir(quota, dir): |
| 465 … | + time.sleep(0.01) |
| 466 … | + pruned = True |
| 467 … | + if pruned: self.ErrorList.append("WARNING: Folder limit reached and pruned!") |
| 468 … | + |
| 469 … | + |
| 470 … | + try: |
| 471 … | + with open(mFile, 'r') as jsn: |
| 472 … | + jDict = json.load(jsn) |
| 473 … | + jFlat = self.flatten_json(jDict) |
| 474 … | + id3 = TinyTag.get(file) |
| 475 … | + if id3.year: jFlat["release_year"] = id3.year |
| 476 … | + if id3.title: jFlat["title"] = id3.title |
| 477 … | + if id3.artist: jFlat["artist"] = id3.artist |
| 478 … | + if id3.duration: duration = id3.duration |
| 479 … | + if not duration: duration = 0 |
| 480 … | + jFlat["duration"] = duration |
| 481 … | + mf = self.fixDuration(mFile, duration) |
| 482 … | + vf = self.fixDuration(file, duration) |
| 483 … | + p = self.parseFilename(vf) |
| 484 … | + mHash = self.add2IPFS(mf) |
| 485 … | + except Exception as e: |
| 486 … | + er = self.decodeException(e) |
| 487 … | + self.ErrorList.append(f"Metadata problem {file}:\n{er}") |
| 488 … | + |
| 489 … | + |
| 490 … | + try: |
| 491 … | + dir,base,grp,pb,ext,vFile,mFile = p |
| 492 … | + vHash = self.add2IPFS(vFile) |
| 493 … | + if len(vHash) == 46 and jFlat: |
| 494 … | + vSize = os.path.getsize(vFile) |
| 495 … | + jFlat['_filename'] = base |
| 496 … | + if os.path.exists(vFile): |
| 497 … | + os.remove(vFile) |
| 498 … | + else: raise IPFSexception |
| 499 … | + |
| 500 … | + except Exception as e: |
| 501 … | + er = self.decodeException(e) |
| 502 … | + args = (grp, vHash, mHash, base, er) |
| 503 … | + self.ErrorList.append("Grupe=%s vHash=%s mHash=%s vFile=%s\n%s" % args) |
| 504 … | + |
| 505 … | + |
| 506 … | + finally: |
| 507 … | + |
| 508 … | + if vHash.startswith("Qm"): |
| 509 … | + self.SQLrows2Add.append([grp, vHash, vSize, mHash, jFlat]) |
| 510 … | + |
| 511 … | + |
| 512 … | + |
| 513 … | + |
| 514 … | + |
| 515 … | + |
| 516 … | + def callback(self, d): |
| 517 … | + if d['status'] == 'finished': |
| 518 … | + path = d['filename'] |
| 519 … | + nam = path.rsplit('/', 1)[1].split("~^~", 1)[0] |
| 520 … | + th = threading.Thread(name=nam, target=self.processVideo, |
| 521 … | + args=([path]), daemon=True) |
| 522 … | + th.start() |
| 523 … | + |
| 524 … | + |
| 525 … | + |
| 526 … | + |
| 527 … | + |
| 528 … | + |
| 529 … | + |
| 530 … | + |
| 531 … | + def ytdlProcess(self, conn): |
| 532 … | + sep = SEPARATOR |
| 533 … | + cols = self.Config['MetaColumns'] |
| 534 … | + dlBase = self.Config['DLbase'] |
| 535 … | + dlArch = dlBase + self.Config['DLarch'] |
| 536 … | + dlElog = dlBase + self.Config['DLeLog'] |
| 537 … | + dlOpts = self.Config['DLOpts'] |
| 538 … | + grupeList = self.Config['Grupes'] |
| 539 … | + total = 0 |
| 540 … | + failures = 0 |
| 541 … | + |
| 542 … | + |
| 543 … | + ytdlFileFormat = "/%(id)s" + sep + "%(duration)s"+ sep + ".%(ext)s" |
| 544 … | + |
| 545 … | + dlOpts['ignoreerrors'] = True |
| 546 … | + dlOpts['downloader'] = "aria2c" |
| 547 … | + dlOpts['downloader-args'] = "aria2c:'-c -j 3 -x 3 -s 3 -k 1M'" |
| 548 … | + |
| 549 … | + |
| 550 … | + |
| 551 … | + |
| 552 … | + dlOpts['writeinfojson'] = True |
| 553 … | + dlOpts['progress_hooks']=[self.callback] |
| 554 … | + dlOpts['download_archive'] = dlArch |
| 555 … | + dlOpts['restrictfilenames'] = True |
| 556 … | + eLog = open(dlElog, mode='a+') |
| 557 … | + for grupe in grupeList: |
| 558 … | + if not grupeList[grupe]['Active']: continue |
| 559 … | + self.SQLrows2Add = [] |
| 560 … | + self.ErrorList = [] |
| 561 … | + print("\nBEGIN " + grupe, flush=True) |
| 562 … | + |
| 563 … | + if not os.path.isdir(dlBase+grupe): |
| 564 … | + os.mkdir(dlBase + grupe) |
| 565 … | + |
| 566 … | + |
| 567 … | + dur = grupeList[grupe]['Duration'] |
| 568 … | + if dur != None and dur > 0: |
| 569 … | + dur = "duration > %d" % dur |
| 570 … | + dlOpts['match_filter'] = utils.match_filter_func(dur) |
| 571 … | + elif 'match_filter' in dlOpts.keys(): |
| 572 … | + del dlOpts['match_filter'] |
| 573 … | + |
| 574 … | + |
| 575 … | + sd = grupeList[grupe]['Start'] |
| 576 … | + ed = grupeList[grupe]['End'] |
| 577 … | + if sd != None or ed != None: |
| 578 … | + dr = utils.DateRange(sd, ed) |
| 579 … | + dlOpts['daterange'] = dr |
| 580 … | + elif 'daterange' in dlOpts.keys(): |
| 581 … | + del dlOpts['daterange'] |
| 582 … | + |
| 583 … | + |
| 584 … | + stop = grupeList[grupe]['Stop'] |
| 585 … | + if stop != None and stop > 0: dlOpts['playlistend'] = stop |
| 586 … | + elif 'playlistend' in dlOpts.keys(): |
| 587 … | + del dlOpts['playlistend'] |
| 588 … | + |
| 589 … | + |
| 590 … | + dlOpts['outtmpl'] = dlBase + grupe + ytdlFileFormat |
| 591 … | + |
| 592 … | + urls = grupeList[grupe]['urls'] |
| 593 … | + |
| 594 … | + newUrls = urls |
| 595 … | + |
| 596 … | + yt_dlp.YoutubeDL(dlOpts).download(newUrls) |
| 597 … | + print(f"YOUTUBE-DL PROCESSING COMPLETE for {grupe}", flush=True) |
| 598 … | + |
|
| 599 … | + |
| 600 … | + for th in threading.enumerate(): |
| 601 … | + if th.name != "MainThread": |
| 602 … | + while th.is_alive(): time.sleep(0.1) |
| 603 … | + |
| 604 … | + |
| 605 … | + good, fails = self.processGrupeResults(conn, cols, |
| 606 … | + len(urls), grupe, eLog) |
| 607 … | + total += good |
| 608 … | + failures += fails |
| 609 … | + |
| 610 … | + eLog.close() |
| 611 … | + return total, failures |
| 612 … | + |
| 613 … | + |
| 614 … | + |
| 615 … | + |
| 616 … | + def displaySummary(self, conn): |
| 617 … | + |
| 618 … | + |
| 619 … | + |
| 620 … | + sql = "SELECT COUNT(*), MIN(sqlts) FROM IPFS_HASH_INDEX;" |
| 621 … | + cols = (conn.cursor().execute(sql)).fetchone() |
| 622 … | + total = f"\n{cols[0]} files downloaded and indexed since {cols[1][0:10]}" |
| 623 … | + print(total, flush=True) |
| 624 … | + mail = total + '\n' |
| 625 … | + |
| 626 … | + |
| 627 … | + |
| 628 … | + strt = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d") |
| 629 … | + sql = "SELECT DISTINCT SUBSTR(sqlts, 6, 5) as tme, grupe, count(*) as cnt " |
| 630 … | + sql += "FROM IPFS_HASH_INDEX WHERE sqlts > ? " |
| 631 … | + sql += "GROUP BY grupe ORDER BY sqlts desc;" |
| 632 … | + args = " Date Videos Grupe (Videos Added in the Last 30 Days)" |
| 633 … | + print(args, flush=True) |
| 634 … | + mail += args + '\n' |
| 635 … | + for cols in conn.execute(sql, (strt,)): |
| 636 … | + args = (cols['tme'], cols['cnt'], cols['grupe']) |
| 637 … | + mail += "%5s | %6d | %s\n" % args |
| 638 … | + print("%5s | %6d | %s" % args, flush=True) |
| 639 … | + |
| 640 … | + |
| 641 … | + |
| 642 … | + sql = "SELECT COUNT(*) FROM IPFS_HASH_INDEX WHERE sqlts > ?" |
| 643 … | + dbObj = conn.cursor().execute(sql, (strt,)) |
| 644 … | + total = " Total: " |
| 645 … | + total += "%5d" % dbObj.fetchone()[0] |
| 646 … | + print(total, flush=True) |
| 647 … | + mail += total |
| 648 … | + |
| 649 … | + |
| 650 … | + |
| 651 … | + urls = "" |
| 652 … | + sql = "SELECT grupe, title, vhash " |
| 653 … | + sql += "FROM IPFS_HASH_INDEX " |
| 654 … | + sql += "WHERE DATE(sqlts) = DATE('now', 'localtime', 'start of day');" |
| 655 … | + rows = (conn.cursor().execute(sql)).fetchall() |
| 656 … | + if len(rows) > 0: |
| 657 … | + args = "\nIPFS URLs for files downloaded today:" |
| 658 … | + urls = args + '\n' |
| 659 … | + print(args, flush=True) |
| 660 … | + for col in rows: |
| 661 … | + args = (col['grupe'], col['title'][:48], col['vhash']) |
| 662 … | + text = "%13s | %48s | https://ipfs.io/ipfs/%s" % args |
| 663 … | + urls += text + '\n' |
| 664 … | + print(text, flush=True) |
| 665 … | + else: |
| 666 … | + args = "\nRun complete. No files downloaded today." |
| 667 … | + print(args, flush=True) |
| 668 … | + mail += args |
| 669 … | + return mail, urls |
| 670 … | + |
| 671 … | + |
| 672 … | + def emailResults(self, server, account, subject, origin, to, text): |
| 673 … | + msg = EmailMessage() |
| 674 … | + msg.set_content(text) |
| 675 … | + msg['Subject'] = subject |
| 676 … | + msg['From'] = origin |
| 677 … | + msg['To'] = to |
| 678 … | + |
| 679 … | + |
| 680 … | + |
| 681 … | + |
| 682 … | + |
| 683 … | + |
| 684 … | + def sendMail(srvr, acct, mesg): |
| 685 … | + try: |
| 686 … | + context = ssl.create_default_context() |
| 687 … | + context.check_hostname = False |
| 688 … | + context.verify_mode = ssl.CERT_NONE |
| 689 … | + with smtplib.SMTP(srvr[0], srvr[1]) as emailer: |
| 690 … | + emailer.starttls(context=context) |
| 691 … | + emailer.login(acct[0], acct[1]) |
| 692 … | + emailer.send_message(mesg) |
| 693 … | + except Exception as e: |
| 694 … | + return self.decodeException(e) |
| 695 … | + return 0 |
| 696 … | + |
| 697 … | + for cnt in range(1, EMAIL_RETRIES): |
| 698 … | + unsent = sendMail(server, account, msg) |
| 699 … | + if unsent: |
| 700 … | + print(f"Attempt {cnt}: {unsent}", flush=True) |
| 701 … | + time.sleep(EMAIL_DELAY) |
| 702 … | + else: break |
| 703 … | + |
| 704 … | + |
| 705 … | + |
| 706 … | + |
| 707 … | + |
| 708 … | + |
| 709 … | + |
| 710 … | + |
| 711 … | + def getCmdLineArgs(self): |
| 712 … | + if len(sys.argv) >= 5: |
| 713 … | + sqlDBfile = self.Config = conn = None |
| 714 … | + grupe = grupes = urls = 0 |
| 715 … | + |
| 716 … | + |
| 717 … | + if sys.argv[1] == "-c" and os.path.isfile(sys.argv[2]): |
| 718 … | + with open(sys.argv[2], 'r') as jsn: |
| 719 … | + self.Config = json.load(jsn) |
| 720 … | + meta = self.Config['DLbase'] + self.Config['DLmeta'] |
| 721 … | + if len(meta) > 0: |
| 722 … | + with open(meta, 'r') as jsn: |
| 723 … | + self.Config['MetaColumns'] = json.load(jsn)['MetaColumns'] |
| 724 … | + metaSize = len(self.Config['MetaColumns']) |
| 725 … | + if metaSize > 0: |
| 726 … | + for grupe in self.Config['Grupes']: |
| 727 … | + grupes += 1 |
| 728 … | + urls += len( self.Config['Grupes'][grupe]['urls'] ) |
| 729 … | + print("Database Metadata Columns=%d" % metaSize, flush=True) |
| 730 … | + print("Downloaded groups will be saved in %s" % self.Config['DLbase'], flush=True) |
| 731 … | + print("%d groups, %d urls to process" % (grupes, urls), flush=True) |
| 732 … | + else: self.usage() |
| 733 … | + else: self.usage() |
| 734 … | + |
| 735 … | + |
| 736 … | + if sys.argv[3] == "-d": |
| 737 … | + sqlDBfile = sys.argv[4] |
| 738 … | + conn = self.openSQLiteDB(sqlDBfile) |
| 739 … | + conn.row_factory = sqlite3.Row |
| 740 … | + if conn == None: self.usage() |
| 741 … | + |
| 742 … | + |
| 743 … | + if len(sys.argv) >= 6 and sys.argv[5] == "-g": |
| 744 … | + print(f"Ignoring all grupes in config except {grupe}", flush=True) |
| 745 … | + for grupe in self.Config['Grupes']: |
| 746 … | + if grupe == sys.argv[6]: continue |
| 747 … | + self.Config['Grupes'][grupe]['Active'] = False |
| 748 … | + |
| 749 … | + if not os.path.isdir(self.Config['DLbase']): |
| 750 … | + os.mkdir(self.Config['DLbase']) |
| 751 … | + |
| 752 … | + return self.Config, conn, sqlDBfile |
| 753 … | + else: self.usage() |
| 754 … | + |
| 755 … | + |
| 756 … | + |
| 757 … | + |
| 758 … | + def runScript(self, sqlFile, conn): |
| 759 … | + hash = None |
| 760 … | + if sqlFile is None or conn is None: |
| 761 … | + self.Config, conn, sqlFile = self.getCmdLineArgs() |
| 762 … | + |
| 763 … | + |
| 764 … | + |
| 765 … | + |
| 766 … | + |
| 767 … | + good, fails = self.ytdlProcess(conn) |
| 768 … | + |
| 769 … | + mail, urls = self.displaySummary(conn) |
| 770 … | + conn.execute("vacuum;") |
| 771 … | + conn.execute("vacuum into 'latest.sqlite';") |
| 772 … | + conn.close() |
| 773 … | + |
| 774 … | + |
| 775 … | + |
| 776 … | + |
| 777 … | + if good > 0: |
| 778 … | + hash = self.publishDB("latest.sqlite") |
| 779 … | + args = f"\nThe newest SQLite DB hash is: {hash}\n" |
| 780 … | + if STATIC_DB_HASH: |
| 781 … | + args += "It is always available at:\n" |
| 782 … | + args += f"https://ipfs.io/ipns/{STATIC_DB_HASH}" |
| 783 … | + mail += args |
| 784 … | + print(args + "\n", flush=True) |
| 785 … | + |
| 786 … | + |
| 787 … | + |
| 788 … | + |
| 789 … | + self.updateRunInfo(sqlFile, hash, good, fails) |
| 790 … | + |
| 791 … | + if SEND_EMAIL: |
| 792 … | + self.emailResults(EMAIL_SERVR, EMAIL_LOGIN, |
| 793 … | + EMAIL_SUB1, EMAIL_FROM, EMAIL_LIST, mail) |
| 794 … | + |
| 795 … | + if len(EMAIL_URLS) > 0 and len(urls) > 0: |
| 796 … | + self.emailResults(EMAIL_SERVR, EMAIL_LOGIN, |
| 797 … | + EMAIL_SUB2, EMAIL_FROM, EMAIL_URLS, urls) |
| 798 … | + |
| 799 … | + |
| 800 … | + |
| 801 … | + |
| 802 … | + |
| 803 … | +if __name__ == "__main__": |
| 804 … | + mp = MediaProcessor() |
| 805 … | + mp.runScript(None, None) |
| 806 … | + exit(0) |