From 2179fa91f6904e17d3bc6490a1e8b5e1c48880aa Mon Sep 17 00:00:00 2001 From: Artem Hotenov Date: Tue, 15 Mar 2022 17:17:49 +0300 Subject: [PATCH] chore: :fire: Remove old files of version 2 and version 1 --- LEP-archive-parser.py | 511 --------------------- LEP-downloader.py | 703 ----------------------------- img/LEP-downloader-screen01.png | Bin 20686 -> 0 bytes img/LEP-downloader-screen02.png | Bin 14589 -> 0 bytes img/ver-2-screenshot-01.png | Bin 15748 -> 0 bytes img/ver-2-screenshot-02.png | Bin 15700 -> 0 bytes img/ver-2-screenshot-03.png | Bin 13203 -> 0 bytes luke_english_podcast_downloader.py | 153 ------- 8 files changed, 1367 deletions(-) delete mode 100644 LEP-archive-parser.py delete mode 100644 LEP-downloader.py delete mode 100644 img/LEP-downloader-screen01.png delete mode 100644 img/LEP-downloader-screen02.png delete mode 100644 img/ver-2-screenshot-01.png delete mode 100644 img/ver-2-screenshot-02.png delete mode 100644 img/ver-2-screenshot-03.png delete mode 100644 luke_english_podcast_downloader.py diff --git a/LEP-archive-parser.py b/LEP-archive-parser.py deleted file mode 100644 index c81f31d..0000000 --- a/LEP-archive-parser.py +++ /dev/null @@ -1,511 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2019 Artem Hotenov (@hotenov). Available under the MIT License. -""" - Python 3.5+ script for parsing the all FREE episodes - of Luke's ENGLISH Podcast, available on its archive webpage - https://teacherluke.co.uk/archive-of-episodes-1-149/ - - Prerequisite: - Installing of packages: requests, beautifulsoup4, lxml - -""" - -# Try import required packages -# usually exception messages scare user -try: - import requests - from bs4 import BeautifulSoup - import lxml -except ImportError: - print("This 3 package (requests, beautifulsoup4, lxml) must be installed for script execution.") - print("You can use 'pip' for that:\n\n" + - "pip install requests\n" + - "pip install beautifulsoup4\n" + - "pip install lxml\n") - print("Quit.") - sys.exit(0) - -import urllib.parse -import json -import re -from collections import namedtuple, OrderedDict -import platform - -# If script was executed on Windows -# enable ANSI color codes on Windows using 'ctypes' -if platform.platform(aliased=0, terse=1).find("Windows") > -1: - import ctypes - #print(platform.platform(aliased=0, terse=1)) - # enable ANSI color codes on Windows - kernel32 = ctypes.windll.kernel32 - kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) -import time -import sys - -# Version of the parser script -__version__ = "2.0.11" - - -# time start marker -start_time = time.time() - -# ---------- [Text colorize functions] ---------------- -fg = lambda text, color: "\33[38;5;" + str(color) + "m" + text + "\33[0m" -bg = lambda text, color: "\33[48;5;" + str(color) + "m" + text + "\33[0m" -# ---------- [END: Text colorize functions] ----------- - -keys_for_json = [ - "postId", "seqNumber", "date", "title", - "episode", "postUrl", "postType", "pdfUrl", - "newName", "oldName", "hasAudio", - "audioFiles", "videoFiles", "audioTracks", - "addons", "isUrl404", "pageParsingStatus", - "originalFileName", "originalFileUrl", "fileExtension", - "fileOldName", "fileNewName", "fileStorageUrl", - "adminComment", "wasUpdatedByAdmin"] - -MainKeys = namedtuple("MainKeys", keys_for_json) -# Dictionary keys 'dk' for treatment via dot -dk = MainKeys( - postId = "postId", seqNumber = "seqNumber", date = "date", title = "title", - episode = "episode", postUrl = "postUrl", postType = "postType", pdfUrl = "pdfUrl", - newName = "newName", oldName = "oldName", hasAudio = "hasAudio", - audioFiles = "audioFiles", videoFiles = "videoFiles", audioTracks = "audioTracks", - addons = "addons", isUrl404 = "isUrl404", pageParsingStatus = "pageParsingStatus", - originalFileName = "originalFileName", originalFileUrl = "originalFileUrl", fileExtension = "fileExtension", - fileOldName = "fileOldName", fileNewName = "fileNewName", fileStorageUrl = "fileStorageUrl", - adminComment = "adminComment", wasUpdatedByAdmin = "wasUpdatedByAdmin") - -# post type dictionary (using via dot) -PostType = namedtuple("PostType", "audio text video") -pt = PostType(audio="AUDIO", text="TEXT", video="VIDEO") - -def get_utf8_response(session, page_url): - """ - Get utf-8 Response from URL - """ - # Create 'bad' Response for handling exceptions - bad_req = requests.Response() - bad_req.status_code = 404 - try: - # By default 'allow_redirects' = True for wp.me links - response = session.get(page_url, timeout=(6, 20)) - if response.ok: - # Set response Encoding (speeds up the parsing process) - response.encoding = "utf-8" - return response - else: - return bad_req - except requests.exceptions.ConnectionError: - print(fg("Exception! ", 160) + "Bad Response for URL: " + page_url) - return bad_req - except requests.exceptions.Timeout: - print(fg("Exception! ", 160) + "Timeout for URL: " + page_url) - return bad_req - except: - print(fg("Unknown Exception!: ", 160) + page_url) - return bad_req - - -def get_bs_object(page_content): - """ Return Beautiful Soap object from text with lxml parser""" - return BeautifulSoup(page_content, features="lxml") - -def get_date_from_url(url): - """ Return date as string "YYYY-MM-DD" """ - # Parse Date from URL - YYYY_MM_DD = re.findall(r'(20\d\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])', url) - if YYYY_MM_DD: - date = "-".join(YYYY_MM_DD[0]) - else: - date = "NO DATE" - return date - -# ---------- [Boolean functions (filters for arguments)] ---------------- -def find_appropriate_tag_a(tag): - """ - Return True if all conditions are met - """ - if tag.string != None: - return tag.has_attr('href') and ( - re.compile(r"^https?://teacherluke\.co\.uk|wp\.me|teacherluke\.wordpress\.com").search(tag.attrs['href']) - and not (re.compile("phrasal-verb-a-day").search(tag.attrs['href']) - or re.compile(r"^.{0,2}\[VIDEO\]", re.I).search(tag.string) - or re.compile(r"Website content\].?$").search(tag.string) - or re.compile(r"all Premium").search(tag.string) - or re.compile(r"LEP App").search(tag.string) - or re.compile(r"episode 522").search(tag.string))) - else: - return tag.has_attr('href') and re.compile(r'rock-n-roll-english-podcast|dvd-commentary|british-pop').search(tag.attrs['href']) -# ---------- [END: Boolean functions (filters for arguments)] ----------- - -def new_data_item( - post_id, seq_number, post_title, post_url, old_name, new_name = "", - post_date = "NO DATE", episode_num = 0, post_type = "POST", pdf_url = "", - has_audio = False, mp3_list = [], - videos = [], audio_tracks = [], addons = [], - is_broken_url = False, parsing_status = "OK", - admin_comment = "", was_updated_by_admin = False): - """ - Return data_info item as dict - """ - return { - dk.postId: post_id, - dk.seqNumber: seq_number, - dk.date: post_date, - dk.title: post_title, - dk.episode: episode_num, - dk.postUrl: post_url, - dk.postType: post_type, - dk.pdfUrl: pdf_url, - dk.oldName: old_name, - dk.newName: new_name, - dk.hasAudio: has_audio, - dk.audioFiles: mp3_list, - dk.videoFiles: videos, - dk.audioTracks: audio_tracks, - dk.addons: addons, - dk.isUrl404: is_broken_url, - dk.pageParsingStatus: parsing_status, - dk.adminComment: admin_comment, - dk.wasUpdatedByAdmin: was_updated_by_admin} - -def ask_uer_to_print_json(): - """ - Asking user to print or not resulting json file - If an exception occurred while writing a file - """ - user_input = input(fg("Would you like to print it on screen? (Y/N): ", 87)) - if user_input in ["Y", "Yes", "YES"]: - print(json.dumps(result_db, ensure_ascii=False, indent=4)) - else: - print(bg(fg("Database file was NOT saved or printed.", 16), 136)) - pass - - -def get_stat_dict(data_dict, result_key, result_val, has_key, negation = False): - """Return filtered dict for statistics""" - if negation: - filtered_dict = { - v[result_key] : v[result_val] - for k, v in data_dict.items() if not v[has_key] - } - else: - filtered_dict = { - v[result_key] : v[result_val] - for k, v in data_dict.items() if v[has_key] - } - return filtered_dict - - - -# Open session -s = requests.Session() -s.max_redirects = 10 - - -# Get page content with list of posts -archive_page_url = "https://teacherluke.co.uk/archive-of-episodes-1-149/" -resp = get_utf8_response(s, archive_page_url) - -if not resp.ok: - print("\n- - - - - -\n" + bg("FAILED", 88)+ ": Script cannot get content of this URL:\n" - + archive_page_url + "\n- - - - - -\n") - sys.exit(0) # is equivalent to exit(0) - -soup = get_bs_object(resp.text) - -# Get only main div container -posts_div = soup.find('div', {'class': 'entry-content'}) -if posts_div == None: - print(bg("\nEMPTY", 88) +": Cannot find div container with class='entry-content' on this URL:\n" - + archive_page_url - + fg("\nQuit.\n", 228)) - sys.exit(0) - -print(fg("\n... Parsing START ... \n\n", 208)) - -# Replace href in tag 'a' with typo in URL "...luke.co.ukm" -try: - tag_for_replace = posts_div.find('a', href=re.compile("london-olympics-2012")) -except AttributeError: - print(fg("Not a soup object! Cannot replace tag here. Skip", 225)) - pass -if tag_for_replace == None: - print(fg("Cannot find tag for replacing! Skip.", 225)) - pass -else: - tag_for_replace['href'] = r'https://teacherluke.co.uk/2012/08/06/london-olympics-2012/' - -#--IMPORTANT: Broke the URL if you need parse all posts again -url_to_lep_db = "https://hotenov.com/d/lep/raw-lep-db.json" -# Read dirty database file from web -print(fg("Getting database file from web.", 8)) -resp = s.get(url_to_lep_db) -resp.encoding = "utf-8" -# Load data from json to dict -if resp.ok: - web_lep_db = OrderedDict(resp.json()) - items_in_lep_db = len(web_lep_db) - print(fg("Database from server was downloaded, all items = ", 8) + str(items_in_lep_db)) -else: - print(bg("404", 88) + ": Database is unavailable, "+ fg("parsing all pages...", 228)) - items_in_lep_db = 0 - web_lep_db = OrderedDict() - - -# Filter only appropriate links to post -cleaned_posts = posts_div.find_all(find_appropriate_tag_a) -all_post_number = len(cleaned_posts) -print(fg("There are: ", 8) + str(all_post_number) + fg(" posts on the website now.", 8)) - -if items_in_lep_db < all_post_number: - updates_delta = all_post_number - items_in_lep_db - # Get only new posts - cleaned_posts = posts_div.find_all(find_appropriate_tag_a, limit=updates_delta) - print(fg(str(updates_delta), 228) + " new posts " + fg("will be processed...", 228)) -else: - print(fg("There are NO NEW posts from the last website parsing.", 34) + fg("\nQuit.", 228)) - sys.exit(1) - -# Iterate all tags with filtered links -# and populate dict 'all_links' -if cleaned_posts: - # Dictionary "Title - URL" from list using "dictionary comprehension" - all_links = {tag_a.text: tag_a['href'] for tag_a in cleaned_posts} -else: - print(fg("No appropriate links on this page", 160)) - print(fg("\nQuit.\n", 228)) - sys.exit(0) - -# Create main dictionary with link info -info_data = OrderedDict() - -# Iteration counter for getting seqNumber -iter_counter = 0 - -# Iterate each URL and parse information from page -for link_text, link_url in all_links.items(): - # Get seqNumber in reverse order - seq_number = all_post_number - iter_counter - iter_counter += 1 - - prefix_id = "P" + str(seq_number).zfill(4) + "-" - - # Getting title from post list (NOT from page title), - # because several old posts have one URL - post_title = str(link_text).strip() - - # Get Response by URL, - post_resp = get_utf8_response(s, link_url) - - post_date = get_date_from_url(post_resp.url) if post_resp.url else "NO DATE" - - # Set old and new file names (without extension) - old_file_name = re.sub('[@,\"^:;*|\\/?><=\\\\/]', '_', post_title).strip() - new_file_name = "[" + post_date + "]" + " # " + old_file_name.strip() - - # For unavailable pages (404) - if post_resp.status_code == 404 or not post_resp.ok: - post_id_404 = prefix_id + "c404" - post_url_404 = link_url - post_date = get_date_from_url(link_url) - # if was redirect before bad status - if resp.history: - # Get last distenetion URL from redirects history - post_url_404 = resp.history[-1].next.url - post_date = get_date_from_url(resp.history[-1].next.url) - new_file_name = "[" + post_date + "]" + " # " + old_file_name.strip() - # Add info about this page - info_data[post_id_404] = new_data_item( - post_id = post_id_404, - seq_number = seq_number, - post_date = post_date, - post_title = link_text, - post_url = post_url_404, - old_name = old_file_name, - new_name = new_file_name, - is_broken_url = True, - parsing_status = "UNKNOWN" - ) - # Go to next link - print(bg("404", 88) + ": Page is unavailable: " + post_title) - continue - - # Get final post URL (mostly for wp.me links) - post_url = post_resp.url - - # Create BeautifulSoup object for one page - post_soap = get_bs_object(post_resp.text) - - # Getting episode number, and postId - ep_num = 0 - post_id = str(ep_num) - if re.search(r'^\d+(?=.)', post_title): - ep_num = int(re.search(r'^\d+(?=.)', post_title).group(0)) - # Combine post_id as "P0001-0001" - post_id = prefix_id + str(ep_num).zfill(4) - else: - # if there is no episode number get 32 leading characters from title - post_id = prefix_id + post_title[:32].strip() - - # Mark default post type as "TEXT" using named tuple - has_audio, post_type = False, pt.text - - # Get all mp3 links with 'Download' keyword - audio_mp3_links = [] - entry_div = post_soap.find('div', {'class': 'entry-content'}) - if entry_div == None: - print(bg(fg("NOT VALID.", 16), 136) + " Cannot parse the page: " + post_title +" --> Post marked as \"NOT VALID\"\n") - # Populate main dictionary - info_data[post_id] = new_data_item( - post_id = post_id, - seq_number = seq_number, - post_date = post_date, - post_title = post_title, - post_url = post_url, - old_name = old_file_name, - new_name = new_file_name, - parsing_status = "NOT VALID" - ) - # Go to next link - continue - else: - tag_a_mp3 = entry_div.find_all('a', href=re.compile(".mp3$")) - for tag_item in tag_a_mp3: - if re.search(r'download|right[-.]click', tag_item.text, re.I): - audio_mp3_links.append(tag_item['href']) - - # Collect info about mp3 downloads into list - mp3_files_info = [] - - if audio_mp3_links: - # Mark post as "AUDIO" - has_audio, post_type = True, pt.audio - - # File part suffix - file_part = "" - - for ind, dlink in enumerate(audio_mp3_links, 1): - # Add "[Part X]" to file name if there are more than one link - if ind > 1: - file_part = " [Part " + str(ind) + "]" - # One line - #file_part = " [Part " + str(ind+1) + "]" if ind > 0 else "" - - # Getting original file name from url: - download_url = dlink - origin_file_name = re.compile(r'[^/]*$').search(download_url).group(0) - - # Get extension (file format) - extension = re.compile(r'\.[^.]*$').search(origin_file_name).group(0) - - # Form filename for saving on disc - old_file_name_with_part = old_file_name + file_part - new_file_name_with_part = new_file_name + file_part - - d_nested_info = {} - d_nested_info[dk.originalFileName] = origin_file_name - d_nested_info[dk.originalFileUrl] = download_url - d_nested_info[dk.fileOldName] = old_file_name_with_part - d_nested_info[dk.fileNewName] = new_file_name_with_part - d_nested_info[dk.fileStorageUrl] = download_url - d_nested_info[dk.fileExtension] = extension - mp3_files_info.append(d_nested_info) - else: - print(bg("No downloads.", 18) + " For page: " + post_title) - - # Generate pdf url (files must be be added on server manually) - server_dowload_url = "https://hotenov.com/d/lep/" - file_name_to_URL = urllib.parse.quote(new_file_name) - url_to_pdf = server_dowload_url + file_name_to_URL + ".pdf" - - # Populate main dictionary - info_data[post_id] = new_data_item( - post_id = post_id, - seq_number = seq_number, - post_date = post_date, - post_title = post_title, - episode_num = ep_num, - post_url = post_url, - post_type = post_type, - pdf_url = url_to_pdf, - old_name = old_file_name, - new_name = new_file_name, - has_audio = has_audio, - mp3_list = mp3_files_info - ) - - -# Reverse dict for following convenience -reversed_info_data = OrderedDict(reversed(info_data.items())) - -# Merge two dictionaries (new posts and existing from database) -print(fg("\nAppending new " + str(len(reversed_info_data)) + " posts " + - "to " + str(items_in_lep_db) + " existing items...", 8)) - -result_db = {**web_lep_db, **reversed_info_data} -print(fg("Done.", 8)) - -print(fg("\n... Parsing END ...", 208)) - -# time end marker -elapsed_time = time.time() - start_time - -# Save parsed database to json file -result_json_file = "raw-lep-db.json" -print(fg("\nWriting", 228) + " merged items to \"" + str(result_json_file) + "\" file...") -try: - with open(result_json_file, 'w', encoding='utf-8') as f: - json.dump(result_db, f, ensure_ascii=False, indent=4) - print(fg("Done.", 8)) -except PermissionError: - print(fg("Access denied!", 160) + " No permission to write the file.") - ask_uer_to_print_json() -except: - print(fg("No possibility to write a file. ", 160)) - ask_uer_to_print_json() - - -print("\n---------- STATISTICS -----------") -# Statistics information -print("All parsed posts: " + str(len(result_db))) - -# Get dict with all AUDIO posts -posts_with_audio = get_stat_dict(result_db, dk.title, dk.postUrl, dk.hasAudio) -if posts_with_audio: - print(" -- with audio: " + str(len(posts_with_audio))) - # Print dict if you need - #for k,v in posts_with_audio.items(): - # print("\n " + k + "\n " + v) - -# Get dict with all not AUDIO posts -posts_without_audio = get_stat_dict(result_db, dk.title, dk.postUrl, dk.hasAudio, True) -if posts_without_audio: - print(" -- NO audio: " + str(len(posts_without_audio))) - # Print dict if you need - #for k,v in posts_without_audio.items(): - # print("\n " + k + "\n " + v) - -# Get dict with all NOT available pages -not_available_pages = get_stat_dict(result_db, dk.title, dk.postUrl, dk.isUrl404) -if not_available_pages: - print("\nNOT available pages: " + str(len(not_available_pages))) - # Print them - for k,v in not_available_pages.items(): - print("\n " + k + "\n " + v) - -# Get dict with all NOT recognizable pages -not_recognizable_pages = { - v[dk.title] : v[dk.postUrl] - for k, v in result_db.items() if v[dk.pageParsingStatus] == "NOT VALID" -} -if not_recognizable_pages: - print("\nNot recognizable pages: " + str(len(not_recognizable_pages))) - # Print them - for k,v in not_recognizable_pages.items(): - print("\n " + k + "\n " + v) - -print("\n -- Execution time: " + str(elapsed_time) + " --") -print("---------- ********** -----------") - diff --git a/LEP-downloader.py b/LEP-downloader.py deleted file mode 100644 index 688fb63..0000000 --- a/LEP-downloader.py +++ /dev/null @@ -1,703 +0,0 @@ -#!/usr/bin/env python3 -# coding=utf-8 -# Copyright (c) 2019 Artem Hotenov (@hotenov). Available under the MIT License. -""" - Python 3.5+ script (and command-line program) for downloading the all FREE episodes - of Luke's ENGLISH Podcast, available on its archive webpage - https://teacherluke.co.uk/archive-of-episodes-1-149/ - -""" - -# Using pure urllib in order to avoid 'requests' installing -from urllib.request import Request, urlopen -from urllib.error import URLError, HTTPError - -import sys -import json -from pathlib import Path -import re -from collections import OrderedDict, namedtuple -import argparse -from operator import itemgetter - -import ssl # For macOS terminal users -import platform # For Windows cmd users - -# If script was executed on Windows -# enable ANSI color codes on Windows using 'ctypes' -if platform.platform(aliased=0, terse=1).find("Windows") > -1: - import ctypes - #print(platform.platform(aliased=0, terse=1)) - # enable ANSI color codes on Windows - kernel32 = ctypes.windll.kernel32 - kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) - -# Version of the downloader script -__version__ = "2.0.5" - -# URLs to json database file and its extends file -url_to_downloads_location = "https://hotenov.com/d/lep/" -url_to_raw_lep_db = url_to_downloads_location + "raw-lep-db.json" -url_to_lep_db_extends = url_to_downloads_location + "lep-db-extends.json" - -# Set default values for variables: -# result database -lep_db = OrderedDict() -# episode interval -ep = "0-xxx" -# Start Date for filtering -start_date = "2000-01-01" -# End Date for filtering -end_date = "2999-12-31" - -# Default folder path for downloading (where script is located) -downloads_folder = Path(".") - -keys_for_json = [ - "postId", "seqNumber", "date", "title", - "episode", "postUrl", "postType", "pdfUrl", - "newName", "oldName", "hasAudio", - "audioFiles", "videoFiles", "audioTracks", - "addons", "isUrl404", "pageParsingStatus", - "originalFileName", "originalFileUrl", "fileExtension", - "fileOldName", "fileNewName", "fileStorageUrl"] - -MainKeys = namedtuple("MainKeys", keys_for_json) -# Dictionary keys 'dk' for treatment via dot -dk = MainKeys( - postId = "postId", seqNumber = "seqNumber", date = "date", title = "title", - episode = "episode", postUrl = "postUrl", postType = "postType", pdfUrl = "pdfUrl", - newName = "newName", oldName = "oldName", hasAudio = "hasAudio", - audioFiles = "audioFiles", videoFiles = "videoFiles", audioTracks = "audioTracks", - addons = "addons", isUrl404 = "isUrl404", pageParsingStatus = "pageParsingStatus", - originalFileName = "originalFileName", originalFileUrl = "originalFileUrl", fileExtension = "fileExtension", - fileOldName = "fileOldName", fileNewName = "fileNewName", fileStorageUrl = "fileStorageUrl" -) - -# post type dictionary (using via dot) -PostType = namedtuple("PostType", "audio text video") -pt = PostType(audio="AUDIO", text="TEXT", video="VIDEO") - -# ---------- [Text colorize functions] ---------------- -fg = lambda text, color: "\33[38;5;" + str(color) + "m" + text + "\33[0m" -bg = lambda text, color: "\33[48;5;" + str(color) + "m" + text + "\33[0m" -# ---------- [END: Text colorize functions] ----------- - - -def get_dict_from_json(url_to_json): - """ - Return OrderedDict from json file - """ - ordered_dict_from_json = OrderedDict() - if args.verbose: - print(fg("Read json file from web.", 8)) - # Tell macOS terminal that our SSL context will be unverified - ssl._create_default_https_context = ssl._create_unverified_context - # Get json file from web - req = Request(url_to_json) - try: - response = urlopen(req) - except HTTPError: - pass - #print('The server couldn\'t fulfill the request.') - #print('Error code: ', e.code) - except URLError: - pass - #print('We failed to reach a server.') - #print('Reason: ', e.reason) - else: - with response as f: - res_body = f.read() - data = json.loads(res_body.decode("utf-8")) - ordered_dict_from_json = OrderedDict(data) - return ordered_dict_from_json - - -def save_dict_to_json(dictionary, path_to_file): - """ - Save dictionary to json file - """ - print(fg("\nWriting", 228) + " merged database file \"" + str(path_to_file) + "\" ...") - try: - with open(path_to_file, 'w', encoding='utf-8') as f: - json.dump(dictionary, f, ensure_ascii=False, indent=4) - print(fg("Done.", 8)) - except PermissionError: - print(fg("Access denied!", 160) + " No permission to write the file.") - except FileNotFoundError: - print(fg("File Not Found!", 160) + " File or directory doesn't exists. Check your full path.") - except: - print(fg("No possibility to write a file. ", 160)) - - -def get_max_episode_number(lep_dictionary): - """ - Return max episode number - """ - max_episode = 0 - for v in lep_dictionary.values(): - if v['episode'] > max_episode: - max_episode = v['episode'] - return max_episode - - -def print_post_titles(full_dict): - """ - Print list of posts' titles in passed dictionary - """ - for k,v in full_dict.items(): - print("› " + v['title'] + " (ID=\'" + k + "\')") - - -def stop_execution_and_exit(msg=""): - """ - Wait user pressing Enter and exit from the script - """ - try: - input(bg(fg(msg + " Press \"Enter\" key to exit: ", 16), 136)) - print() - sys.exit(0) - except EOFError: - sys.exit(0) - - -def get_items_by_episode_num(full_dict, episode_str): - """ - Get filtered items by episode number from main database - Episode param as string, with parsing start and end episodes - """ - items_by_episode_num = OrderedDict() - parse_str = str(episode_str).strip() - start_num = 0 - end_num = 0 - - if parse_str != "": - match = re.match(r"(?P^\d{0,5})(?P-)?(?Pxxx)?(?P\d{0,5})", parse_str) - g_start = match.group('g_start') - g_delimiter = match.group('g_delimiter') - g_infinity = match.group('g_infinity') - g_end = match.group('g_end') - if g_start and g_delimiter == "-" and g_end: - start_num = int(g_start) - end_num = int(g_end) - elif g_start and g_delimiter == "-" and g_infinity == "xxx": - start_num = int(g_start) - end_num = get_max_episode_number(full_dict) - elif g_start: - start_num = int(g_start) - end_num = int(g_start) - else: - print("Incorrect episode number") - return items_by_episode_num - - if start_num > end_num: - start_num, end_num = end_num, start_num - - items_by_episode_num = { - k: v - for k,v in full_dict.items() if start_num <= v[dk.episode] <= end_num} - return items_by_episode_num - else: - return items_by_episode_num - - -def is_valid_date(date_string): - """ - Validate if the passed date string has format "YYYY-MM-DD" - """ - regex = re.compile(r"(2[0-9]\d\d)[-](0[1-9]|1[012])[-](0[1-9]|[12][0-9]|3[01])") - match = regex.match(str(date_string)) - return bool(match) - -def get_items_by_date(full_dict, start_date, end_date): - """ - Get filtered items by publish date from main database - Dates params as string "YYYY-MM-DD" - """ - date_start = start_date - date_end = end_date - if date_start > date_end: - date_start, date_end = date_end, date_start - items_filtered_by_date = OrderedDict() - items_filtered_by_date = { - k: v - for k,v in full_dict.items() if date_start <= v[dk.date] <= date_end} - return items_filtered_by_date - -def get_audio_items(full_dict): - """ - Return new OrderedDict filtered by hasaudio attribute - """ - items_by_has_audio = { - k: v - for k,v in full_dict.items() if v[dk.hasAudio]} - return items_by_has_audio - -def get_audiotrack_items(full_dict): - """ - Return new OrderedDict filtered by having audioTracks elements - """ - items_by_has_audiotracks = { - k: v - for k,v in full_dict.items() if v[dk.audioTracks]} - return items_by_has_audiotracks - - -def get_only_text_items(full_dict): - """ - Return new OrderedDict filtered by postType = TEXT - """ - items_text = { - k: v - for k,v in full_dict.items() if v[dk.postType] == "TEXT"} - return items_text - - -def get_only_video_items(full_dict): - """ - Return new OrderedDict filtered by postType = VIDEO - """ - items_video = { - k: v - for k,v in full_dict.items() if v[dk.postType] == "VIDEO"} - return items_video - - -def get_text_and_video_items(full_dict): - """ - Return new OrderedDict filtered by hasaudio attribute = False - """ - items_by_has_not_audio = { - k: v - for k,v in full_dict.items() if not v[dk.hasAudio]} - return items_by_has_not_audio - -def get_list_with_all_files(full_dict, file_section): - """ - Return new list of files in passed section (audioFiles, videoFiles, audioTracks or addons) - """ - all_files_in_section = OrderedDict() - all_files_in_section = [ - file_item - for k,v in full_dict.items() - for file_item in v[file_section]] - return all_files_in_section - -def get_pdf_list(full_dict): - """ - Return list of pdf files - """ - pdf_files = [] - for v in full_dict.values(): - # Make dict with pdf file as other files - # For using one method of getting files - item_iter = { - "fileOldName": "", - "fileNewName": v['newName'], - "fileStorageUrl": v['pdfUrl'], - "fileExtension": ".pdf"} - pdf_files.append(item_iter) - return pdf_files - - -def get_length(collection): - """ - Return length of collection as str - """ - return str(len(collection)) - -def get_filenames_in_folder(folder): - """ - Return list of filenames (stem + suffix) in folder - """ - files_in_folder = [p for p in folder.iterdir() if p.is_file()] - filenames_in_folder = [f.stem + f.suffix for f in files_in_folder] - return filenames_in_folder - -def get_folder_scan_info(folder, downloads): - """ - Return list of three nested lists: - existing_old_files, existing_files, non_exesting_files - """ - # Get list of filenames in folder - filenames = get_filenames_in_folder(folder) - - # Create three empty list - existing_old_files = [] - existing_files = [] - non_exesting_files = [] - - for download in downloads: - if download['fileOldName'] + download['fileExtension'] in filenames: - existing_old_files.append(download) - elif download['fileNewName'] + download['fileExtension'] in filenames: - existing_files.append(download) - else: - non_exesting_files.append(download) - - return [existing_old_files, existing_files, non_exesting_files] - -def print_scan_dir_info(scan_info_list): - """ - Print information from three nested list - """ - if scan_info_list[0]: - print("\nThere are " + get_length(scan_info_list[0]) + " existing file(s) with OLD name:") - for df in scan_info_list[0]: - print("› " + df['fileOldName'] + df['fileExtension']) - - if scan_info_list[1]: - print("\nThere are " + get_length(scan_info_list[1]) + " existing file(s):") - for df in scan_info_list[1]: - print("› " + df['fileNewName'] + df['fileExtension']) - - if scan_info_list[2]: - print("\n" + get_length(scan_info_list[2]) + " non-existing file(s) will be download:") - for df in scan_info_list[2]: - print("› " + df['fileNewName'] + df['fileExtension']) - -def download_files(downloads_list, folder): - """ - Download all files in the folder from list - """ - if downloads_list: - print(fg("\nDownloading...\n", 228)) - for item in downloads_list: - filename = item['fileNewName'] + item['fileExtension'] - path_to_file = folder / filename - if not path_to_file.exists(): - #resp = s.get(item['fileStorageUrl']) - ssl._create_default_https_context = ssl._create_unverified_context - req = Request(item['fileStorageUrl']) - try: - response = urlopen(req) - except HTTPError: - not_found_files.append(item) - print(" " + fg("-", 160) + " " + filename + " " + bg("404: NOT FOUND", 88)) - pass - #print('The server couldn\'t fulfill the request.') - #print('Error code: ', e.code) - except URLError: - not_found_files.append(item) - print(" " + fg("-", 160) + " " + filename + " " + bg("404: NOT FOUND", 88)) - pass - #print('We failed to reach a server.') - #print('Reason: ', e.reason) - else: - #bin_data = response.read() - try: - with open(path_to_file, 'wb') as f: - bin_data = response.read() - f.write(bin_data) - downloaded_files.append(item) - print(" " + fg("+", 34) + " " + filename + " " + bg(fg("DOWNLOADED", 255), 34) + fg(" +", 34)) - except PermissionError: - # print(fg("Access denied!", 160) + " No permission to write the file.") - unsaved_files.append(item) - print(" " + fg("-", 160) + " " + filename + " " + bg("UNSAVED", 124) + fg(" -", 160)) - except FileNotFoundError: - #print(fg("File Not Found!", 160) + " File or directory doesn't exists. Check your full path.") - unsaved_files.append(item) - print(" " + fg("-", 160) + " " + filename + " " + bg("UNSAVED", 124) + fg(" -", 160)) - except: - #print(fg("No possibility to write a file. ", 160)) - # If empty file was created - delete it - if path_to_file.exists(): - path_to_file.unlink() - unsaved_files.append(item) - print(" " + fg("-", 160) + " " + filename + " " + bg("UNSAVED", 124) + fg(" -", 160)) - else: - exesting_files.append(item) - print(" " + fg("•", 245) + " " + filename + " " + bg(fg("ON DISC", 16), 250) + fg(" •", 245)) - print(fg("\nDownloading END.", 228)) - else: - print("\nAll files are on the disc already. There are NO NEW files to download in selected episode(s).") - # -TODO: Insert link to documentation page - print("You can try another script options. For example, to download video files use option --onlyvideo ") - print("See all them here ** or using --help option.") - -def get_parser(): - """Return parser for args""" - help_prog = "LEP downloader" - help_usage = "python3 LEP-downloader.py [OPTIONS]" - help_description = "Python 3.5+ script for downloading the all FREE episodes of Luke's ENGLISH Podcast, available on its archive webpage" - parser = argparse.ArgumentParser(prog=help_prog, usage=help_usage, description=help_description) - parser.add_argument("-ep", "--episode", type=str, - help="Specify episode number (or range of episodes) for downloading") - parser.add_argument("-pdf", "--withpdf", action="store_true", - help="Download PDF files of exported webpage(s)") - parser.add_argument("-vi", "--onlyvideo", action="store_true", - help="Download only video files") - parser.add_argument("-to", "--folder", type=str, metavar="PATH", - help="Specify a path to custom download directory (folder)") - parser.add_argument("-d1", "--startdate", type=str, - help="Start date for date range filtering. Format \"YYYY-MM-DD\"") - parser.add_argument("-d2", "--enddate", type=str, - help="End date for date range filtering. Format \"YYYY-MM-DD\"") - parser.add_argument("--last", action="store_true", - help="Download only the last episode. Options -ep, -d1 and -d2 will be ignored in this case.") - parser.add_argument("-text" , "--withoutanymedia", action="store_true", - help="Print names list of the posts which have no any media files (only TEXT) for selected episode(s). When used along with --withpdf script downloads pdf files for these posts") - parser.add_argument("-v", "--verbose", action="store_true", - help="Activate verbose mode to display execution steps results") - parser.add_argument("-q", "--quiet", action="store_true", - help="Activate quiet mode. There is no question whether to download files or not. Immediately download them.") - parser.add_argument("-V", "--version", action="version", version="'%(prog)s' ver. " + __version__, - help="Print script version and exit") - parser.add_argument("-sj", "--savejson", action="store_true", - help="Save database to file \"lep-db.json\" in the same folder" ) - parser.add_argument("--withaddons", action="store_true", - #help=argparse.SUPPRESS, - help="Download all additional files (if they added by script author) for selected episode(s)") - return parser - - -parser = get_parser() -args = parser.parse_args() - -# Define empty objects -filtered_items = OrderedDict() - -all_audio_files = [] -all_audiotracks_files = [] -all_pdf_files = [] -all_video_files = [] -all_addons_files = [] -all_downloads = [] - -print(fg("START script execution...\n", 208)) - -# Look at episode number argument -if args.episode and (not args.last or not args.withoutanymedia): - print("Selected number(s) of episode(s): " + args.episode + "\n") - ep = args.episode -else: - # Ignore '--episode' argument if '--last' argument is specified - if args.last: - print("Selected number(s) of episode(s): " + "The last" + "\n") - # Ignore '--episode' argument if '--withoutanymedia' argument is specified - elif args.withoutanymedia: - print("Selected number(s) of episode(s): " + "only TEXT posts" + "\n") - else: - print("Selected number(s) of episode(s): " + "ALL" + "\n") - -# Load parsed (raw) database -raw_lep_db = get_dict_from_json(url_to_raw_lep_db) - -# If database is unavailable (or empty) -if not raw_lep_db: - print("\nSorry. Json file with main database " + fg("is unavailable now.", 170) + " Try again later.") - stop_execution_and_exit() -else: - if args.verbose: - print(fg("Raw LEP Database was downloaded, all items = " + get_length(raw_lep_db), 8)) - - -# Load extends with corrections and additional files -lep_db_extends = get_dict_from_json(url_to_lep_db_extends) - -# If extends database is unavailable (or empty) -if not lep_db_extends: - print("\nSorry. Json file with extends database " + fg("is unavailable now.", 170) + " Try again later if you want to deal with it.") - print(fg("Raw database", 208) + " (without extends) " + fg("will be processed.", 208)) - lep_db = raw_lep_db -else: - # Merge main db with extends - lep_db = OrderedDict({**raw_lep_db, **lep_db_extends}) - if args.verbose: - print(fg("Extends database file was downloaded, all corrected items = " + get_length(lep_db_extends), 8)) - print(fg("Databases was merged, all items = " + get_length(lep_db), 8)) - -# Get posts items filtered by episode number -filtered_by_episode = get_items_by_episode_num(lep_db, ep) -if args.verbose: - print(fg("\nAll filtered by episode: " + get_length(filtered_by_episode), 8)) - - -filtered_items = filtered_by_episode - -# if --startdate OR --enddate arguments are specified -if args.startdate or args.enddate: - if args.startdate and not args.enddate: - start_date = args.startdate - elif not args.startdate and args.enddate: - end_date = args.enddate - else: - start_date = args.startdate - end_date = args.enddate - if not is_valid_date(start_date): - print("Your start date \"" + start_date + "\" is " + fg("the wrong format!", 170) + " It was reset by default to \"2000-01-01\"") - start_date = "2000-01-01" - if not is_valid_date(end_date): - print("Your end date \"" + end_date + "\" is " + fg("the wrong format!", 170) + " It was reset by default to \"2999-12-31\"") - end_date = "2999-12-31" - filtered_by_date = get_items_by_date(filtered_by_episode, start_date, end_date) - if args.verbose: - print(fg("\nPosts filtered by date \"" + start_date + "\" to \"" + end_date + "\": " + get_length(filtered_by_date), 8)) - filtered_items = filtered_by_date - -# If "--last" argument is specified -if args.last: - print(fg("\n** You specified the \"--last\" option. Only the last episode will be downloaded **", 208)) - last_item = lep_db.popitem() - filtered_items = OrderedDict({last_item[0]: last_item[1]}) - print(fg("The last episode (post) is: ", 8) + last_item[1]['title']) - -# If "--withoutanymedia" argument is specified -if args.withoutanymedia: - items_with_text = get_only_text_items(filtered_items) - print(fg("Total posts without any media (audio or video): " + get_length(items_with_text), 8)) - if args.withpdf: - filtered_items = items_with_text - else: - print_post_titles(items_with_text) - print(fg("\nHint:", 39) + - "\nUse options \"-text --withpdf\" if you want to download pdf files for these posts.") - sys.exit(0) - -# Get dictionaries for statistics by post type in user query -stat_audio_posts = get_audio_items(filtered_items) -stat_video_posts = get_only_video_items(filtered_items) -stat_text_posts = get_only_text_items(filtered_items) - -# If "--onlyvideo" argument is specified -if args.onlyvideo: - print(fg("\n** You specified to process ONLY VIDEO episode(s) **", 208)) - items_with_video = get_only_video_items(filtered_items) - if args.verbose: - print(fg("\nAll VIDEO posts in selected episode(s): " + get_length(items_with_video), 8)) - filtered_items = items_with_video - all_video_files = get_list_with_all_files(filtered_items, dk.videoFiles) - print(fg("\nAll video files: " + get_length(all_video_files), 8)) -else: - items_with_audio = get_audio_items(filtered_items) - if args.verbose: - print(fg("\nAll AUDIO posts in selected episode(s): " + get_length(items_with_audio), 8)) - items_with_audiotracks = get_audiotrack_items(filtered_items) - if args.verbose: - print(fg("\nAll VIDEO posts in selected episode(s) also having audio tracks: " + get_length(items_with_audiotracks), 8)) - - all_audio_files = get_list_with_all_files(items_with_audio, dk.audioFiles) - if args.verbose: - print(fg("\nAll audio files: " + get_length(all_audio_files), 8)) - - all_audiotracks_files = get_list_with_all_files(items_with_audiotracks, dk.audioTracks) - if args.verbose: - print(fg("\nAll audiotracks files: " + get_length(all_audiotracks_files), 8)) - -# If "--withpdf" argument is specified -if args.withpdf: - all_pdf_files = get_pdf_list(filtered_items) - if args.verbose: - print(fg("\nAll pdf files: " + get_length(all_pdf_files), 8)) - -# If "--withaddons" argument is specified -if args.withaddons: - all_addons_files = get_list_with_all_files(filtered_items, dk.addons) - if args.verbose: - print(fg("\nAll addons files: " + get_length(all_addons_files), 8)) - -# Merge all lists with files to one -all_downloads = [*all_audio_files, *all_audiotracks_files, *all_pdf_files, *all_video_files, *all_addons_files] - -# Sort list with all files for downloading by 'fileNewName' -# It looks like sorting by post publishing date ASC (the last post at the end of list) -sorted_downloads = sorted(all_downloads, key=itemgetter('fileNewName')) -# Another way to do it without itemgetter: sorted(all_downloads, key=lambda e: e['fileNewName']) - -# If "--folder" argument is specified -if args.folder and all_downloads: - # Try to make folder if it doesn't exist (check path) - try: - Path(args.folder).mkdir(parents=True, exist_ok=True) - except PermissionError: - #print(fg("Access denied!", 160) + " No permission to write in the folder \"" + str(Path(args.folder)) + "\"") - print(fg("Access denied!", 160) + " No permission to write in the folder \"" + str(Path(args.folder)) + "\"") - stop_execution_and_exit() - except FileNotFoundError: - #print(fg("File Not Found!", 160) + " File or directory doesn't exists. Check your full path.") - print(fg("Access denied!", 160) + " No permission to write in the folder \"" + str(Path(args.folder)) + "\"") - stop_execution_and_exit() - except: - #print(fg("No possibility to write a file. ", 160)) - print(fg("Access denied!", 160) + " No permission to write in the folder \"" + str(Path(args.folder)) + "\"") - stop_execution_and_exit() - # If folder exists or script managed to create it then change default value - downloads_folder = Path(args.folder) - -# You can save result json file if you need -# If "--savejson" argument is specified -if args.savejson: - # Save result database to json file - file_to_save_db = downloads_folder / "lep-db.json" - save_dict_to_json(lep_db, file_to_save_db) - -# Scan folder for downlaoding to check for the existing files -scan_dir_info = get_folder_scan_info(downloads_folder, sorted_downloads) -# If "--verbose" argument is specified print this information -if args.verbose: - print_scan_dir_info(scan_dir_info) - -# Lists for statistics -exesting_files = [*scan_dir_info[0], *scan_dir_info[1]] -downloaded_files = [] -not_found_files = [] -unsaved_files = [] - -if args.quiet: - # Donload files from "non-existing" list - download_files(scan_dir_info[2], downloads_folder) -else: - if scan_dir_info[2]: - user_input = input(fg("Would download " + get_length(scan_dir_info[2]) + " file(s). Proceed (y/n)?: ", 87)) - if user_input in ["y", "Y", "Yes", "YES"]: - download_files(scan_dir_info[2], downloads_folder) - else: - stop_execution_and_exit("You canceled the file downloading.") - else: - print("\nAll files are on the disc already. There are NO NEW files to download in selected episode(s).") - print("You can try another script options. For example, to download video files use option --onlyvideo ") - print("See all them here: cutt.ly/LEP-downlaoder or using --help option.") - - -# Print statistics -print() -print(fg("{:-^30s}".format(" STATISTICS "), 8)) -if args.verbose: - print("All processed posts: " + bg("{:8d}".format(len(filtered_items)), 240)) - print(fg(" ••• ", 8) + bg(fg("AUDIO", 255), 34) + " = " + "{:13d}".format(len(stat_audio_posts))) - print(fg(" ••• ", 8) + bg("VIDEO", 13) + " = " + "{:13d}".format(len(stat_video_posts))) - print(fg(" ••• ", 8) + bg("TEXT", 18) + " = " + "{:14d}".format(len(stat_text_posts))) - print("All processed files: " + bg("{:8d}".format(len(all_downloads)), 240)) - if not args.onlyvideo: - print(fg(" ••• ", 8) + "audio = " + "{:13d}".format(len(all_audio_files))) - print(fg(" ••• ", 8) + "audio tracks = " + "{:6d}".format(len(all_audiotracks_files))) - if args.withpdf: - print(fg(" ••• ", 8) + "pdf = " + "{:15d}".format(len(all_pdf_files))) - if args.withaddons: - print(fg(" ••• ", 8) + "addons = " + "{:12d}".format(len(all_addons_files))) - else: - print(fg(" ••• ", 8) + "video = " + "{:13d}".format(len(all_video_files))) - if args.withpdf: - print(fg(" ••• ", 8) + "pdf = " + "{:15d}".format(len(all_pdf_files))) - if args.withaddons: - print(fg(" ••• ", 8) + "addons = " + "{:12d}".format(len(all_addons_files))) - - print() - print("Downloading results:") -print(bg(fg("DOWNLOADED", 255), 34) + " = " + "{:6d}".format(len(downloaded_files))) -print(bg(fg("ON DISC", 16), 250) + " = " + "{:9d}".format(len(exesting_files))) -if not_found_files: - print(bg("NOT FOUND", 88) + " = " + "{:7d}".format(len(not_found_files))) -if unsaved_files: - print(bg("UNSAVED", 124) + " = " + "{:9d}".format(len(unsaved_files))) -print(fg("{:-^30s}".format(" ********** "), 8)) - -print(fg("\nEND script execution.", 208)) - -if not args.quiet: - stop_execution_and_exit() - - - - diff --git a/img/LEP-downloader-screen01.png b/img/LEP-downloader-screen01.png deleted file mode 100644 index b4184042c3da8188814135193e91eb288ba8e72e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20686 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 diff --git a/img/ver-2-screenshot-01.png b/img/ver-2-screenshot-01.png deleted file mode 100644 index d6f740466b8c5bda638776eeb4cf5f246d2b97f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15748 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 diff --git a/img/ver-2-screenshot-02.png b/img/ver-2-screenshot-02.png deleted file mode 100644 index 3be880aa80433e6ee728a7b295c8a2020b762505..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15700 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 PVz0_`b%io{i@^T{$FGUm diff --git a/luke_english_podcast_downloader.py b/luke_english_podcast_downloader.py deleted file mode 100644 index e8b8d63..0000000 --- a/luke_english_podcast_downloader.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Download all Luke’s ENGLISH Podcast files(only mp3 files) listed in https://teacherluke.co.uk/archive-of-episodes-1-149/. -You don't need to open every link and right click to download Podcastself. -'python3 luke_english_podcast_downloader.py' will do the job for you. - -Author: Pavan J -""" -""" -Updating by 'hotenov': -(2019-03-13) -- excluding illegal characters from filename (via regex thanks to 'Enigma' http://network.ubotstudio.com/forum/index.php/topic/17406-tutorial-regex-illegal-characters-in-path/#entry105434 ) -- exception handling (thanks to 'XavierCLL' and 'Arpad Horvath' https://stackoverflow.com/a/36896675/3366563 and 'ProfHase85' https://stackoverflow.com/a/21407552/3366563) -- displaying of current processing link (file) with colorized status (thanks to 'rabin utam' https://stackoverflow.com/a/21786287/3366563) -- downloading all mp3 files on the page (for multipart episodes and links to other materials) -""" - -from requests_html import HTMLSession -from urllib import request -import os -import re -from urllib import error -from requests.exceptions import ConnectionError - -# COUNTERS # -on_disc = 0 -downloaded = 0 -mp3_broken = 0 -page_not_found = 0 -no_audio = 0 -all_posts = 0 - -# Start session -session = HTMLSession() -r = session.get('https://teacherluke.co.uk/archive-of-episodes-1-149/') - -# Find all links in 'entry-content' class, containing in 'href' attribute with "https://teacherluke.co.uk" or "wp.me" strings -all_links = r.html.find('.entry-content a[href*="https://teacherluke.co.uk"], .entry-content a[href*="wp.me"]') - -for link in reversed(all_links): # Reversed for processing from 1: latest on site - latest on disc - all_posts += 1 - # Replace illegal characters - target_file = re.sub('[@,\"^:;*|\\/?><=\\\\/]', '_', link.text) + '.mp3' - # If episode is already downloaded - if os.path.exists(target_file): - print("file: {title} \x1b[0;30;47m{status}\x1b[0m".format(title=link.full_text, link=link.attrs['href'], status="ON DISC")) - on_disc += 1 - # go to next link - continue - try: - # Check in try block that link is available, with redirects (for several wp.me links) - resp = session.head(link.attrs['href'], allow_redirects=True) - except ConnectionError as e: - print("file: {title} \x1b[1;30;41m{status}\x1b[0m [{link}]".format(title=link.full_text, link=link.attrs['href'], status="PAGE NOT FOUND")) - resp = "No response" - page_not_found +=1 - except error.HTTPError as e: - # Return code error (e.g. 404, 501, ...) - print('HTTPError: {code} LINK={link}'.format(code = e.code, link = link.attrs['href'])) - page_not_found +=1 - except error.URLError as e: - # Not an HTTP-specific error (e.g. connection refused) - print('URLError: {}'.format(e.reason)) - page_not_found +=1 - else: - # 200 - print("file: {title} ".format(title=link.full_text, link=link.attrs['href']), end='') - sub_r = session.get(resp.url) - # Find all links with key words in 'entry-content' class - download_link_a = sub_r.html.find('.entry-content a', containing=['DOWNLOAD', 'Download', 'Right-click']) - # Calculate the number of links - all_downloaded_links = len(download_link_a) - # If there is one or more - if all_downloaded_links > 0: - part = 1 - for part_link in reversed(download_link_a): - # Take only links to mp3 files - if part_link and '.mp3' in part_link.attrs['href']: - mp3_link = part_link.attrs['href'] - try: - open_mp3 = request.urlopen(mp3_link) - except error.HTTPError as e: - # Return code error (e.g. 404, 501, ...) - print('\x1b[0;30;41m{status}\x1b[0m Details: {code} [{link_to_file}]'.format(code=e.code,link_to_file=mp3_link,status="MP3 FILE BROKEN")) - mp3_broken += 1 - continue - except error.URLError as e: - # Not a HTTP-specific error (e.g. connection refused) - print('URLError: {}'.format(e.reason)) - mp3_broken += 1 - continue - else: - # 200 - mp3_data = open_mp3.read() - # if there is only one download link - if part < 2: - # Write file on disc - with open(target_file, 'wb') as audio: - audio.write(mp3_data) - print("\x1b[1;37;42m{status}\x1b[0m".format(status="DOWNLOADED")) - downloaded += 1 - part += 1 - # If there are another parts of episode or links to other mp3 materials. In this case add '[Part X]' to the end of file - else: - target_file = re.sub('[@,\"^:;*|\\/?><=\\\\/]', '_', link.text) + ' [Part ' + str(part) + ']' + '.mp3' - with open(target_file, 'wb') as audio: - audio.write(mp3_data) - print("file: {title} [Part {part}] \x1b[1;37;42m{status}\x1b[0m".format(status="DOWNLOADED", part=part, title=link.full_text)) - downloaded += 1 - part += 1 - # If NO mp3 dowloads - else: - if part < 2: - print("\x1b[5;30;43m{status}\x1b[0m ".format(status="NO AUDIO")) - no_audio += 1 - else: - print("file: {title} [Part {part}] \x1b[1;37;42m{status}\x1b[0m".format(status="NO AUDIO", part=part, title=link.full_text)) - no_audio += 1 - # If NO downloads links at all - else: - print("\x1b[5;30;43m{status}\x1b[0m".format(status="NO AUDIO")) - no_audio += 1 - -info_text = """ -\x1b[0;35;40mThere are episodes which have no direct link for downloading, but you can listen to them in player on the website. Here are a couple of them:\x1b[0m -128. Luke’s Stand-Up Comedy Show (is lost by Luke, but remains online) -> \x1b[0;36;40mhttps://cutt.ly/LEP-ep128\x1b[0m -Luke’s Interview on InglesPodcast -> \x1b[0;36;40mhttp://www.inglespodcast.com/2015/03/31/mansion-interviews-luke-thompson-from-lukes-english-podcast/\x1b[0m -[Website content] Luke on the RealLife English Podcast -> \x1b[0;36;40mhttps://teacherluke.co.uk/2017/09/24/website-content-luke-on-the-real-life-english-podcast/\x1b[0m -I was invited onto the “English Across The Pond” Podcast -> \x1b[0;36;40mhhttps://teacherluke.co.uk/2017/05/26/i-was-invited-onto-the-english-across-the-pond-podcast/\x1b[0m - -\x1b[0;35;40mAnd one wrong link (typo):\x1b[0m -London Olympics 2012 -> \x1b[0;36;40mhttps://teacherluke.co.uk/2012/08/06/london-olympics-2012/\x1b[0m -and so on... -\x1b[0;35;40mRelevant: 19 October 2019\x1b[0m - -I highly recommend you the new version of this script cutt.ly/LEP-downlaoder -It works much better and downloads all such episodes! -""" - - -# display summary -print("--------------------") -print("ALL Links - {counter}".format(counter=all_posts)) -print("\x1b[1;37;42m{status}\x1b[0m - {counter}".format(status="DOWNLOADED", counter=downloaded)) -print("\x1b[0;30;47m{status}\x1b[0m - {counter}".format(status="ON DISC", counter=on_disc)) -print("\x1b[5;30;43m{status}\x1b[0m - {counter}".format(status="NO AUDIO", counter=no_audio)) -print("\x1b[0;30;41m{status}\x1b[0m - {counter}".format(status="MP3 FILE BROKEN", counter=mp3_broken)) -print("\x1b[1;30;41m{status}\x1b[0m - {counter}".format(status="PAGE NOT FOUND", counter=page_not_found)) -print("--------------------") -print(info_text) -print("\x1b[0;32;40mDone!\x1b[0m")