diff --git a/scripts/plexEmail.py b/scripts/plexEmail.py index 610fa53..1ecbf52 100644 --- a/scripts/plexEmail.py +++ b/scripts/plexEmail.py @@ -27,255 +27,258 @@ SCRIPT_VERSION = 'v0.9.3' +reload(sys) +sys.setdefaultencoding("utf-8") + def replaceConfigTokens(): ## The below code is for backwards compatibility if ('filter_include_original_title' not in config): config['filter_include_original_title'] = True - + if ('logging_enabled' not in config): config['logging_enabled'] = True - + if ('plex_web_server_guid' not in config): config['plex_web_server_guid'] = '' - + if ('upload_cloudinary_use_https' not in config): config['upload_cloudinary_use_https'] = True - + if ('logging_retain_previous_logs' not in config): config['logging_retain_previous_logs'] = True - + if ('logging_debug_level' not in config): config['logging_debug_level'] = 'INFO' - + if ('logging_file_location' not in config): config['logging_file_location'] = '' - + if ('upload_cloudinary_api_secret' not in config): config['upload_cloudinary_api_secret'] = True - + if ('artist_sort_1' not in config.keys() or config['artist_sort_1'] == ""): config['artist_sort_1'] = 'title_sort' - + if ('artist_sort_1_reverse' not in config.keys() or config['artist_sort_1_reverse'] == ""): config['artist_sort_1_reverse'] = False if ('artist_sort_2_reverse' not in config.keys() or config['artist_sort_2_reverse'] == ""): config['artist_sort_2_reverse'] = False if ('artist_sort_3_reverse' not in config.keys() or config['artist_sort_3_reverse'] == ""): config['artist_sort_3_reverse'] = False - + if ('album_sort_1' not in config.keys() or config['album_sort_1'] == ""): config['album_sort_1'] = 'title_sort' - + if ('album_sort_1_reverse' not in config.keys() or config['album_sort_1_reverse'] == ""): config['album_sort_1_reverse'] = False if ('album_sort_2_reverse' not in config.keys() or config['album_sort_2_reverse'] == ""): config['album_sort_2_reverse'] = False if ('album_sort_3_reverse' not in config.keys() or config['album_sort_3_reverse'] == ""): config['album_sort_3_reverse'] = False - + if ('msg_new_artists_header' not in config): config['msg_new_artists_header'] = 'New Artists' - + if ('msg_new_albums_header' not in config): config['msg_new_albums_header'] = 'New Albums' - + if ('msg_new_songs_header' not in config): config['msg_new_songs_header'] = 'New Songs' - + if ('msg_artists_link' not in config): config['msg_artists_link'] = 'Artists' - + if ('msg_albums_link' not in config): config['msg_albums_link'] = 'Albums' - + if ('msg_songs_link' not in config): config['msg_songs_link'] = 'Songs' - + if ('filter_show_artists' not in config): config['filter_show_artists'] = True - + if ('filter_show_albums' not in config): config['filter_show_albums'] = True - + if ('filter_show_songs' not in config): config['filter_show_songs'] = False - + if ('filter_sections_Music' not in config): config['filter_sections_Music'] = {'tagline':{'order':1,'show':False,'preText':'','postText':'','include':[],'exclude':[]},'summary':{'order':2,'show':True,'preText':'','postText':'','include':[],'exclude':[]},'tags_genre':{'order':3,'show':True,'preText':'Genre(s): ','postText':'','include':[],'exclude':[]},'tags_director':{'order':4,'show':False,'preText':'Director: ','postText':'','include':[],'exclude':[]},'tags_star':{'order':5,'show':False,'preText':'Star(s): ','postText':'','include':[],'exclude':[]},'content_rating':{'order':6,'show':False,'preText':'ContentRating: ','postText':'','include':[],'exclude':[]},'duration':{'order':7,'show':True,'preText':'Runtime: ','postText':' minutes','include':[],'exclude':[]},'year':{'order':8,'show':False,'preText':'Year: ','postText':'','include':[],'exclude':[]},'studio':{'order':9,'show':True,'preText':'Network: ','postText':'','include':[],'exclude':[]},'rating':{'order':10,'show':False,'preText':'Rating: ','postText':'%','include':[],'exclude':[]},'air_date':{'order':11,'show': True,'preText':'Release Date: ','postText':'','include':[],'exclude':[],'format': '%B %d, %Y'},'track_list':{'order':12,'show': True,'headerText':{'trackNumber':'Track#','songName':'Song Name','artistName':'Artist(s)','duration':'Duration'}}} - + if ('filter_artists_include' not in config): config['filter_artists_include'] = [] - + if ('filter_artists_exclude' not in config): config['filter_artists_exclude'] = [] - + if ('filter_albums_include' not in config): config['filter_albums_include'] = [] - + if ('filter_albums_exclude' not in config): config['filter_albums_exclude'] = [] - + if ('filter_songs_include' not in config): config['filter_songs_include'] = [] - + if ('filter_songs_exclude' not in config): config['filter_songs_exclude'] = [] - + if ('filter_song_thumbnail_type' not in config): config['filter_song_thumbnail_type'] = 'album' - + if ('filter_movies_include' not in config): config['filter_movies_include'] = [] - + if ('filter_movies_exclude' not in config): config['filter_movies_exclude'] = [] - + if ('filter_shows_include' not in config): config['filter_shows_include'] = [] - + if ('filter_shows_exclude' not in config): config['filter_shows_exclude'] = [] - + if ('filter_seasons_include' not in config): config['filter_seasons_include'] = [] - + if ('filter_seasons_exclude' not in config): config['filter_seasons_exclude'] = [] - + if ('filter_episodes_include' not in config): config['filter_episodes_include'] = [] - + if ('filter_episodes_exclude' not in config): config['filter_episodes_exclude'] = [] - + if ('filter_episode_thumbnail_type' not in config): config['filter_episode_thumbnail_type'] = 'episode' - + if ('email_unsubscribe' not in config): config['email_unsubscribe'] = [] - + if ('email_use_bcc' not in config): config['email_use_bcc'] = False - + if ('email_to_send_to_shared_users' not in config): config['email_to_send_to_shared_users'] = False - + if ('plex_username' not in config): config['plex_username'] = '' - + if ('plex_password' not in config): config['plex_password'] = '' - + if ('filter_show_email_images' not in config): config['filter_show_email_images'] = True - + if ('msg_notice' not in config): config['msg_notice'] = '' - + if ('email_use_ssl' not in config): config['email_use_ssl'] = False - + if ('filter_include_plex_web_link' not in config): config['filter_include_plex_web_link'] = True - + if ('filter_libraries' not in config): config['filter_libraries'] = [] - + if ('filter_sections_movies' not in config): config['filter_include_sections_movies'] = {'tagline':{'order':1,'show':True,'preText':'','postText':'','include':[],'exclude':[]},'summary':{'order':2,'show':True,'preText':'','postText':'','include':[],'exclude':[]},'tags_genre':{'order':3,'show':True,'preText':'Genre(s): ','postText':'','include':[],'exclude':[]},'tags_director':{'order':4,'show':False,'preText':'Director: ','postText':'','include':[],'exclude':[]},'tags_star':{'order':5,'show':True,'preText':'Star(s): ','postText':'','include':[],'exclude':[]},'content_rating':{'order':6,'show':False,'preText':'ContentRating: ','postText':'','include':[],'exclude':[]},'duration':{'order':7,'show':True,'preText':'Runtime: ','postText':' minutes','include':[],'exclude':[]},'year':{'order':8,'show':True,'preText':'Year: ','postText':'','include':[],'exclude':[]},'studio':{'order':9,'show':False,'preText':'Studio: ','postText':'','include':[],'exclude':[]},'rating':{'order':10,'show':True,'preText':'Rating: ','postText':'%','include':[],'exclude':[]}} - + if ('filter_sections_TV' not in config): config['filter_include_sections_TV'] = {'tagline':{'order':1,'show':True,'preText':'','postText':'','include':[],'exclude':[]},'summary':{'order':2,'show':True,'preText':'','postText':'','include':[],'exclude':[]},'tags_genre':{'order':3,'show':False,'preText':'Genre(s): ','postText':'','include':[],'exclude':[]},'tags_director':{'order':4,'show':False,'preText':'Director: ','postText':'','include':[],'exclude':[]},'tags_star':{'order':5,'show':False,'preText':'Star(s): ','postText':'','include':[],'exclude':[]},'content_rating':{'order':6,'show':False,'preText':'ContentRating: ','postText':'','include':[],'exclude':[]},'duration':{'order':7,'show':True,'preText':'Runtime: ','postText':' minutes','include':[],'exclude':[]},'year':{'order':8,'show':False,'preText':'Year: ','postText':'','include':[],'exclude':[]},'studio':{'order':9,'show':True,'preText':'Network: ','postText':'','include':[],'exclude':[]},'rating':{'order':10,'show':False,'preText':'Rating: ','postText':'%','include':[],'exclude':[]}} - + if ('web_only_save_images' not in config): config['web_only_save_images'] = False - + if ('filter_show_movies' not in config): config['filter_show_movies'] = True - + if ('filter_show_shows' not in config): config['filter_show_shows'] = True - + if ('filter_show_seasons' not in config): config['filter_show_seasons'] = True - + if ('filter_show_episodes' not in config): config['filter_show_episodes'] = True - + if ('date_minutes_back_to_search' not in config): config['date_minutes_back_to_search'] = 0 - + if ('msg_no_new_content' not in config): config['msg_no_new_content'] = 'There have been no new additions to Plex from {past_day} through {current_day}.' - + if ('email_skip_if_no_additions' not in config): config['email_skip_if_no_additions'] = False - + if ('web_skip_if_no_additions' not in config): config['web_skip_if_no_additions'] = False - + if ('date_days_back_to_search' not in config and 'days_back_to_search' in config): config['date_days_back_to_search'] = config['days_back_to_search'] - + if ('movie_sort_1' not in config.keys() or config['movie_sort_1'] == ""): config['movie_sort_1'] = 'rating' if ('movie_sort_2' not in config.keys() or config['movie_sort_2'] == ""): config['movie_sort_2'] = 'title_sort' - + if ('movie_sort_1_reverse' not in config.keys() or config['movie_sort_1_reverse'] == ""): config['movie_sort_1_reverse'] = True if ('movie_sort_2_reverse' not in config.keys() or config['movie_sort_2_reverse'] == ""): config['movie_sort_2_reverse'] = False if ('movie_sort_3_reverse' not in config.keys() or config['movie_sort_3_reverse'] == ""): config['movie_sort_3_reverse'] = False - + if ('show_sort_1' not in config.keys() or config['show_sort_1'] == ""): config['show_sort_1'] = 'title_sort' - + if ('show_sort_1_reverse' not in config.keys() or config['show_sort_1_reverse'] == ""): config['show_sort_1_reverse'] = False if ('show_sort_2_reverse' not in config.keys() or config['show_sort_2_reverse'] == ""): config['show_sort_2_reverse'] = False if ('show_sort_3_reverse' not in config.keys() or config['show_sort_3_reverse'] == ""): config['show_sort_3_reverse'] = False - + if ('season_sort_1' not in config.keys() or config['season_sort_1'] == ""): config['season_sort_1'] = 'title_sort' if ('season_sort_2' not in config.keys() or config['season_sort_2'] == ""): config['season_sort_2'] = 'index' - + if ('season_sort_1_reverse' not in config.keys() or config['season_sort_1_reverse'] == ""): config['season_sort_1_reverse'] = False if ('season_sort_2_reverse' not in config.keys() or config['season_sort_2_reverse'] == ""): config['season_sort_2_reverse'] = False if ('season_sort_3_reverse' not in config.keys() or config['season_sort_3_reverse'] == ""): config['season_sort_3_reverse'] = False - + if ('episode_sort_1' not in config.keys() or config['episode_sort_1'] == ""): config['episode_sort_1'] = 'show_title_sort' if ('episode_sort_2' not in config.keys() or config['episode_sort_2'] == ""): config['episode_sort_2'] = 'season_index' if ('episode_sort_3' not in config.keys() or config['episode_sort_3'] == ""): config['episode_sort_3'] = 'index' - + if ('episode_sort_1_reverse' not in config.keys() or config['episode_sort_1_reverse'] == ""): config['episode_sort_1_reverse'] = False if ('episode_sort_2_reverse' not in config.keys() or config['episode_sort_2_reverse'] == ""): config['episode_sort_2_reverse'] = False if ('episode_sort_3_reverse' not in config.keys() or config['episode_sort_3_reverse'] == ""): config['episode_sort_3_reverse'] = False - + # The below code is replacing tokens if (config['web_domain'].rfind('/') < len(config['web_domain']) - len('/')): config['web_domain'] = config['web_domain'] + '/' - + if (config['date_days_back_to_search'] == 31): now = datetime.datetime.now() if (now.month == 1): config['date_days_back_to_search'] = calendar.monthrange(now.year - 1, 12)[1] else: config['date_days_back_to_search'] = calendar.monthrange(now.year, now.month - 1)[1] - + for value in config: if (isinstance(config[value], str)): config[value] = config[value].replace('{past_day}', str((date.today() - timedelta(days=config['date_days_back_to_search'], hours=config['date_hours_back_to_search'], minutes=config['date_minutes_back_to_search'])).strftime(config['date_format']))) @@ -285,20 +288,20 @@ def replaceConfigTokens(): if (config['plex_data_folder'].rfind(os.path.sep) < len(config['plex_data_folder']) - len(os.path.sep)): config['plex_data_folder'] = config['plex_data_folder'] + os.path.sep - + if (config['plex_data_folder'].find('Plex Media Server') >= 0): config['plex_data_folder'] = config['plex_data_folder'][0:config['plex_data_folder'].find('Plex Media Server')] - + if (config['web_folder'].rfind(os.path.sep) < len(config['web_folder']) - len(os.path.sep)): config['web_folder'] = config['web_folder'] + os.path.sep - + if (config['web_domain'] != '' and config['web_domain'].rfind('/') < len(config['web_domain']) - len('/')): config['web_domain'] = config['web_domain'] + '/' - + if (config['logging_file_location'].rfind(os.path.sep) < len(config['logging_file_location']) - len(os.path.sep)): config['logging_file_location'] = config['logging_file_location'] + os.path.sep - - + + def convertToHumanReadable(valuesToConvert): convertedValues = {} for value in valuesToConvert: @@ -323,7 +326,7 @@ def getSharedUserEmails(): emails = [] if (config['plex_username'] == '' or config['plex_password'] == ''): return emails - + url = 'https://my.plexapp.com/users/sign_in.json' logging.info('getSharedUserEmails: url = ' + url) base64string = 'Basic ' + base64.encodestring('%s:%s' % (config['plex_username'], config['plex_password'])).replace('\n', '') @@ -334,7 +337,7 @@ def getSharedUserEmails(): logging.info('getSharedUserEmails: response = ' + str(response.text)) token = json.loads(response.text)['user']['authentication_token']; logging.info('getSharedUserEmails: token = ' + token) - + url = 'https://plex.tv/pms/friends/all' logging.info('getSharedUserEmails: url = ' + url) headers = {'Accept': 'application/json', 'X-Plex-Token': token} @@ -342,14 +345,14 @@ def getSharedUserEmails(): response = requests.get(url, headers=headers) logging.info('getSharedUserEmails: response = ' + str(response)) logging.info('getSharedUserEmails: response = ' + str(response.text.encode('ascii', 'ignore'))) - + parsed = XML(response.text.encode('ascii', 'ignore')) for elem in parsed: for name, value in sorted(elem.attrib.items()): if (name == 'email'): logging.info('getSharedUserEmails: adding email - ' + value.lower()) emails.append(value.lower()) - + logging.info('getSharedUserEmails: Returning shared emails') logging.debug('getSharedUserEmails: email list - ' + ' '.join(emails)) return emails @@ -363,7 +366,7 @@ def deleteImages(): logging.debug('deleteImages: deleting image: ' + folder + file) os.remove(folder + file) logging.info('deleteImages: end') - + def processImage(imageHash, thumb, mediaType, seasonIndex, episodeIndex): logging.info('processImage: begin') logging.info('processImage: imageHash = ' + imageHash + ' - thumb = ' + thumb + ' - mediaType = ' + mediaType + ' - seasonIndex = ' + str(seasonIndex) + ' - episodeIndex = ' + str(episodeIndex)) @@ -374,7 +377,7 @@ def processImage(imageHash, thumb, mediaType, seasonIndex, episodeIndex): thumbObj['webImgPath'] = '' thumbObj['emailImgPath'] = '' return thumbObj - + if (thumb.find('http://') >= 0 or thumb.find('https://') >= 0): logging.info('processImage: thumb is already an externally hosted image') thumbObj['webImgPath'] = thumb @@ -421,12 +424,12 @@ def processImage(imageHash, thumb, mediaType, seasonIndex, episodeIndex): imgName += '_' + imageHash webImgFullPath = config['web_domain'] + config['web_path'] + '/images/' + imgName + '.jpg' img = config['web_folder'] + config['web_path'] + os.path.sep + 'images' + os.path.sep + imgName + '.jpg' - + logging.info('processImage: imgLocation = ' + imgLocation) logging.info('processImage: webImgFullPath = ' + webImgFullPath) logging.info('processImage: img = ' + img) - - + + cloudinaryURL = '' if ('upload_use_cloudinary' in config and config['upload_use_cloudinary']): logging.info('processImage: Uploading to cloudinary') @@ -443,7 +446,7 @@ def processImage(imageHash, thumb, mediaType, seasonIndex, episodeIndex): else: logging.info('processImage: No email image') thumbObj['emailImgPath'] = '' - + if (cloudinaryURL != ''): logging.info('processImage: Setting image paths to cloudinary') thumbObj['webImgPath'] = cloudinaryURL @@ -460,16 +463,16 @@ def processImage(imageHash, thumb, mediaType, seasonIndex, episodeIndex): thumbObj['webImgPath'] = 'images/' + imgName + '.jpg' else: thumbObj['webImgPath'] = '' - + return thumbObj - + def uploadToImgur(imgToUpload, nameOfUpload): client_id = 'a0c7b089b62b220' headers = {"Authorization": "Client-ID " + client_id} url = "https://api.imgur.com/3/upload.json" if (os.path.isfile(imgToUpload)): response = requests.post( - url, + url, headers = headers, data = { 'image': b64encode(open(imgToUpload, 'rb').read()), @@ -482,7 +485,7 @@ def uploadToImgur(imgToUpload, nameOfUpload): return data['link'] else: return '' - + def uploadToCloudinary(imgToUpload): logging.info('uploadToCloudinary: begin') if (os.path.isfile(imgToUpload)): @@ -501,16 +504,16 @@ def uploadToCloudinary(imgToUpload): else: logging.info('uploadToCloudinary: file not located') return '' - + def containsnonasciicharacters(str): - return not all(ord(c) < 128 for c in str) + return not all(ord(c) < 128 for c in str) def addheader(message, headername, headervalue): if containsnonasciicharacters(headervalue): h = Header(headervalue, 'utf-8') message[headername] = h else: - message[headername] = headervalue + message[headername] = headervalue return message def sendMail(email): @@ -534,18 +537,18 @@ def sendMail(email): # Create the body of the message (a plain-text and an HTML version). text = config['msg_email_teaser'] - + # Record the MIME types of both parts - text/plain and text/html. if containsnonasciicharacters(emailHTML): htmltext = MIMEText(emailHTML, 'html','utf-8') else: - htmltext = MIMEText(emailHTML, 'html') + htmltext = MIMEText(emailHTML, 'html') if(containsnonasciicharacters(text)): - plaintext = MIMEText(text,'plain','utf-8') + plaintext = MIMEText(text,'plain','utf-8') else: plaintext = MIMEText(text,'plain') - + #Create image headers for image in imgNames: fp = open(imgNames[image], 'rb') @@ -574,9 +577,9 @@ def sendMail(email): server.login(gmail_user, gmail_pwd) server.sendmail(FROM, TO, msg.as_string()) server.close() - + return 1; - + def createEmailHTML(): emailText = """ @@ -596,7 +599,7 @@ def createEmailHTML(): - + @@ -604,18 +607,70 @@ def createEmailHTML(): @@ -638,7 +693,7 @@ def createEmailHTML():