commit 0766f23220417aba080ffab0fab38079766d38d9 Author: Sean Date: Thu Feb 5 11:07:42 2026 -0500 Initial Commit diff --git a/environment.py b/environment.py new file mode 100644 index 0000000..fc3c4b7 --- /dev/null +++ b/environment.py @@ -0,0 +1,36 @@ +APPEND_SYS_PATH='/home/pi/.kodi/addons/plugin.video.fox.news/resources/lib' +PATH_VIDEO_DATABASE='/home/pi/.kodi/addons/plugin.video.fox.news/resources/lib/archive' +USE_ICON_URL=True +PATH_LOG_FILE="/home/pi/.kodi/temp/MyLog.log" +FOX_NEWS_URL="https://www.foxnews.com/video" +FOX_NEWS_US_URL="hhttps://www.foxnews.com/video/topics/us" +FOX_NEWS_EXCLUSIVE_URL="https://foxnews.com" +FOX_NEWS_AMERICAS_NEWSROOM_URL="https://www.foxnews.com/video/shows/americas-newsroom" +FOX_NEWS_OUTNUMBERED_URL="https://www.foxnews.com/video/shows/outnumbered" +SELECTED_ARCHIVE_URL="https://www.foxnews.com/video/dummy" +#FOX_NEWS_ICON_OF_LAST_RESORT="https://static.foxnews.com/static/orion/styles/img/fox-news/favicons/apple-touch-icon-180x180.png" +#ENABLE_USE_ICON_OF_LAST_RESORT=False + + +VIDEODB_AMERICAS_NEWSROOM_FILENAME="videodb_americasnewsroom.txt" +VIDEODB_OUTNUMBERED_FILENAME="videodb_outnumbered.txt" +VIDEODB_FILENAME="videodb.txt" +VIDEODB_EXCLUSIVE_FILENAME="videodb_exc.txt" +VIDEODB_US_FILENAME="videodb_us.txt" +ARCHIVEDB_FILENAME="archivedb.txt" + +CACHE_EXPIRY_MINS=10 + +class PathHelper: + pathChar="/" + + def __init__(self): + pass + + @staticmethod + def makePathFileName(file,path): + while file.endswith(PathHelper.pathChar): + file=file[:-1] + while path.endswith(PathHelper.pathChar): + path=path[:-1] + return path+PathHelper.pathChar+file diff --git a/newsfeed.py b/newsfeed.py new file mode 100644 index 0000000..15bc0ff --- /dev/null +++ b/newsfeed.py @@ -0,0 +1,358 @@ +import tkinter as tk +import math +from functools import partial +import json +import webbrowser +import os +import requests +import traceback +import time +import glob +import re +import shutil +from datetime import timedelta +from datetime import datetime +from datetime import timezone +from environment import * +from utility import * + +class Video: + def __init__(self): + self.description=None + self.url=None + self.icon=None + self.timestamp=DateTime() + self.feedTimeOffset='just now' + + def __init__(self, description, url): + self.description=description + self.url=url + self.icon=None + self.timestamp=DateTime() + self.feedTimeOffset='just now' + + def __init__(self, description, url, icon, timestamp=None): + self.description=description + self.url=url + self.icon=icon + if None==timestamp: + self.timestamp=DateTime() + else: + self.timestamp=timestamp + self.feedTimeOffset='just now' + + def getDescription(self): + return self.description + + def getUrl(self): + return self.url + + def getIcon(self): + return self.icon + + def getTimestamp(self): + return self.timestamp + + def setFeedTime(self,feedtime): + self.feedtime=feedtime + + def getFeedTime(self): + return self.feedtime + + def getFeedTimeOffset(self): + return self.feedTimeOffset + + def setFeedTimeOffset(self, feedtimeoffset): + self.feedTimeOffset=feedtimeoffset + + def toString(self): + return(self.description+"|"+self.url+"|"+self.icon+"|"+self.timestamp.toString()) + + @staticmethod + def fromString(line): + splits=line.split("|") + description=splits[0].strip() + url=splits[1].strip() + icon=splits[2].strip() + timestamp=DateTime(splits[3].strip()) + return(Video(description,url,icon,timestamp)) + +class NewsFeed: + def __init__(self, logger=None): + self.logger=logger + + @staticmethod + def isResourceAvailable(url): + try: + response=requests.head(url, timeout=2.5) + if not response.ok: + return False + return True + except: + return False + + def getItemsInFeed(self,url): + now=datetime.now() + sections=Sections() + videos = {} + httpNetRequest=HttpNetRequest() + response=httpNetRequest=httpNetRequest.getHttpNetRequest(url) + status=response.status_code + searchIndex=0 + response.close() + if status!=200: + return None + while -1!= searchIndex: + video, searchIndex= sections.getItemsInSection(response.text,"article",searchIndex) + if video is not None and not (video.description in videos): + videos[video.description]=video + video.setFeedTime(DateTimeHelper.applyRelativeTime(now,video.feedTimeOffset)) + videoList=NewsFeed.filterFeedMaxDays(list(videos.values()),7) +# videoList=list(videos.values()) + videoList=sorted(videoList, key=lambda x:x.getFeedTime(),reverse=True) + return (videoList) + + @staticmethod + def filterFeedMaxDays(videos,days): + now=datetime.now() + filteredList=[] + for video in videos: + delta=now-video.getFeedTime() + if delta.days <= days: + filteredList.insert(0,video) + return filteredList + +class Sections: + def __init__(self): + self.dummy=None + + def getItemsInSection(self, strInput, sectionName, searchIndex): + video=None + startSection='<'+sectionName + endSection='") + if -1 != indexDuration: + strDuration=strContainingString[indexDuration:] + strDuration=self.betweenString(strDuration,">","<") + description=description+" - "+strDuration + indexPublication=strContainingString.index("
") + if -1 != indexPublication: + strPublication=strContainingString[indexPublication:] + strPublication=self.betweenString(strPublication,"") + description=description+" ("+strPublication+")" + icon=None + indexIcon=strContainingString.index("srcset=") + if -1 != indexIcon: + icon=strContainingString[indexIcon:] + icon=self.betweenString(icon,"\"","\"") + splits=icon.split(',') + icon=self.betweenString(splits[len(splits)-1],None,'?') + icon=icon.strip() + video=Video(description,previewUrl,icon) + video.feedTimeOffset=strPublication + return video, searchIndex + + def getVideoIdInSection(self, strInput, sectionName, searchIndex): + video=None + startSection='<'+sectionName + endSection='=length: + return str + while stringLength < length: + sb=sb+filler + stringLength=stringLength+1 + return sb+str + +def parseDuration(strDuration): + expression=re.compile(r"\d+") + result=expression.findall(strDuration) + if 2!=len(result): + return None, None + return pad(result[0],'0',2), pad(result[1],'0',2) + + +def selectedItem(event): + widget=event.widget + selectedIndex=int(widget.curselection()[0]) + itemsInList=len(videoCollection.getVideoList()) + if selectedIndex >=0 and selectedIndex < itemsInList: + videoUrl=videoCollection.getVideoList()[selectedIndex].getUrl() + webbrowser.open(videoUrl) + +def refresh(): + listbox.delete(0,'end') + loadVideos() + +def loadVideos(): + newsfeed=NewsFeed() + videos=newsfeed.getItemsInFeed(FOX_NEWS_URL) + itemIndex=1 + for video in videos: + listbox.insert(itemIndex,video.getDescription()) + itemIndex=itemIndex+1 + videoCollection.setVideoList(videos) + +class VideoCollection: + def __init__(self): + self.videoList=[] + + def getVideoList(self): + return self.videoList + + def setVideoList(self,videoList): + self.videoList=videoList + +videoCollection=VideoCollection() + +master = tk.Tk() +master.geometry("450x300") +#master.geometry("640x480") + +#Frame +frame=tk.Frame(master,height=100) +frame.pack(fill='both',expand='true') + +bottomframe=tk.Frame() +bottomframe.pack(side='bottom') + +#Entry +label_heading = tk.Label(frame, text = "Fox News Video Feed",font='arial 13 bold',background='blue',foreground='white') +label_heading.pack(side='top', fill='both') + +#yscrollbar for list +yscrollbar=tk.Scrollbar(frame) +yscrollbar.pack(side=tk.RIGHT, fill=tk.Y) + +#xscrollbar for list +xscrollbar=tk.Scrollbar(frame, orient=tk.HORIZONTAL) +xscrollbar.pack(side=tk.BOTTOM,fill=tk.X) + +# List +listbox=tk.Listbox(frame,selectmode='single',yscrollcommand=yscrollbar.set,xscrollcommand=xscrollbar.set) +listbox.pack(side='left', fill='both') +listbox.bind('',selectedItem) +listbox.configure(background='skyblue4',foreground='white',font='arial 8 bold',width=0) + +#configure the scrollbar +yscrollbar.config(command=listbox.yview) +xscrollbar.config(command=listbox.xview) + +#Button +btn_refresh = tk.Button(bottomframe,text = 'Refresh',command = refresh) +btn_refresh.pack(side='bottom') + +loadVideos() +listbox.focus_set() + +master.mainloop() diff --git a/utility.py b/utility.py new file mode 100644 index 0000000..f00eea1 --- /dev/null +++ b/utility.py @@ -0,0 +1,231 @@ +import time +import webbrowser +import requests +from datetime import timedelta +from datetime import datetime +from datetime import timezone +from environment import * + +class myLog(): + def __init__(self): + self._file = open(PATH_LOG_FILE,"a",encoding='utf-8') + + def write(self,item): + currentDateTime = DateTimeHelper.getCurrentDateTime() + strCurrentDateTime = DateTimeHelper.getDateTimeAsString(currentDateTime) + strOutput = '[' + strCurrentDateTime +'] '+item + self._file.write(strOutput) + self._file.write("\n") + self._file.flush() + +class StringHelper: + def __init__(self): + pass + + @staticmethod + def betweenString(strItem, strBegin, strEnd): + if strItem is None: + return None + index=-1 + if strBegin is None: + index=0 + else: + index = strItem.index(strBegin) + if -1==index: + return None + str=None + if strBegin is not None: + str=strItem[index+len(strBegin):] + else: + str=strItem + if strEnd is None: + return str + index=str.index(strEnd) + if -1==index : + return None + sb="" + for strIndex in range(0, len(str)-1): + if index==strIndex: + break + sb=sb+str[strIndex] + return (sb) + +class HttpNetRequest: + def __init__(self): + self.Message="" + + def getHttpNetRequest(self,url): + retrycount=0 + maxretries=5 + while retrycount maxretries: + raise + + +class DateTimeHelper: + def __init__(self): + pass + + def __init__(self,capturedatetime,timeChange): + self.datetimestamp=capturedatetime + self.timeChange=timeChange + self.offsetTime=DateTimeHelper.applyRelativeTime(self.datetimestamp, self.timeChange) + + def getDateTimeStamp(self): + return self.datetimestamp + + def getTimeChange(self): + return self.timeChange + + def getOffsetTime(self): + return self.offsetTime + + def getOffsetTimeAsString(self): + return DateTimeHelper.getDateTimeAsString(self.offsetTime) + + def toString(self): + pass + + @staticmethod + def getDateTimeAsString(someDateTime): + if(not isinstance(someDateTime,datetime)): + raise Exception('Invalid type for parameter') + return someDateTime.strftime("%m-%d-%Y %H:%M:%S") + + @staticmethod + def getDateTimeFromString(someDateTimeString): + if(not isinstance(someDateTimeString,str)): + raise Exception('Invalid type for parameter') + return DateTimeHelper.strptime(someDateTimeString,"%m-%d-%Y %H:%M:%S") + + @staticmethod + def getCurrentDateTime(): + return datetime.now() + + @staticmethod + def strptime(theTime,theFormat): + try: + return datetime.strptime(theTime,theFormat) + except: + return datetime(*(time.strptime(theTime,theFormat)[0:6])) + + @staticmethod + def canstrptime(theTime,theFormat): + try: + datetime.strptime(theTime,theFormat) + return True + except: + return False + + + +# returns a datetime + @staticmethod + def applyRelativeTime(sometime,relativetime): + if(not isinstance(sometime,datetime)): + raise Exception('Invalid type for parameter') + if(not isinstance(relativetime,str)): + raise Exception('Invalid type for parameter') + if DateTimeHelper.canstrptime(relativetime,'%B %d, %Y'): + sometime = DateTimeHelper.strptime(relativetime,'%B %d, %Y') + return sometime + if relativetime=='just now': + return sometime + if relativetime=='just in': + return sometime + relativetimesplit=relativetime.split() + if len(relativetimesplit)==2: + year=datetime.now().year + relativetimex=relativetime+', '+str(year) + relativeDate = DateTimeHelper.strptime(relativetimex, '%B %d, %Y') + if(relativeDate>datetime.now()): + year=datetime.now().year-1 + relativetimex=relativetime+', '+str(year) + relativeDate=DateTimeHelper.strptime(relativetimex,'%B %d, %Y') + days=sometime-relativeDate + sometime=sometime-days + elif relativetimesplit[1]=='hour' or relativetimesplit[1]=='hours': + hours=int(relativetimesplit[0]) + sometime=sometime-timedelta(hours=hours) + elif relativetimesplit[1]=='day' or relativetimesplit[1]=='days': + days=int(relativetimesplit[0]) + sometime=sometime-timedelta(days=days) + elif relativetimesplit[1]=='minute' or relativetimesplit[1]=='minutes': + minutes=int(relativetimesplit[0]) + sometime=sometime-timedelta(minutes=minutes) + elif len(relativetimesplit)==3: # '16 mins ago' '2 hours ago' + if relativetimesplit[1]=='mins': + minutes=int(relativetimesplit[0]) + sometime=sometime-timedelta(minutes=minutes) + elif relativetimesplit[1]=='hours': + hours=int(relativetimesplit[0]) + sometime=sometime-timedelta(hours=hours) + elif relativetimesplit[1]=='day' or relativetimesplit[1]=='days': + days=int(relativetimesplit[0]) + sometime=sometime-timedelta(days=days) + return sometime + +class DateTime: + def __init__(self): + self.date=DateTime.getCurrentTime() + + def __init__(self,strDate=None): + if None!=strDate: + self.date=DateTime.dateFromString(strDate) + else: + self.date=DateTime.getCurrentTime() + + def toString(self): + return DateTime.dateToString(self.date) + + def deltaTime(self,someDate): + return DateTime.deltaTime(self.date,someDate) + + @staticmethod + def dateToString(someDate): + return str(someDate) + + @staticmethod + def dateFromString(strDate): + return datetime.fromisoformat(strDate) + + @staticmethod + def now(): + return DateTime.getCurrentTime() + + @staticmethod + def getCurrentTime(): + return datetime.now(timezone.utc) + + @staticmethod + def sortList(dateList): + dateList.sort(key=lambda x:x.toString()) + + @staticmethod + def deltaTime(startTime,endTime): + if startTime > endTime: + timedelta=startTime-endTime + else: + timedelta=endTime-startTime + days, seconds=timedelta.days, timedelta.seconds + hours=timedelta.total_seconds()//3600 + minutes=(seconds %3600)//60 + seconds=seconds%60 + return days, hours, minutes, seconds + + + +#currentDate=DateTimeHelper.getCurrentDateTime() +#strDateTime=DateTimeHelper.getDateTimeAsString(currentDate) +#print(strDateTime) +#relativeTime=DateTimeHelper.applyRelativeTime(currentDate,'October 9') +#strDateTime=DateTimeHelper.getDateTimeAsString(relativeTime) +#print(relativeTime) +#if(relativeTime>currentDate): +# print('It is greater') +