├── Roku.Deploy.exe ├── myvideobuzz.zip ├── .gitignore ├── images ├── About.jpg ├── Search.jpg ├── icon_s.jpg ├── History.jpg ├── Settings.jpg ├── TopRated.jpg ├── icon_key.jpg ├── icon_next.jpg ├── icon_prev.jpg ├── icon_user.jpg ├── TopChannels.jpg ├── icon_search.jpg ├── icon_video.jpg ├── whattowatch.jpg ├── ClearHistory.jpg ├── IndianChannels.jpg ├── MostDiscussed.jpg ├── MostResponded.jpg ├── NurseryRhymes.jpg ├── TopFavorites.jpg ├── YourFavorites.jpg ├── YourPlaylists.jpg ├── icon_barcode.jpg ├── icon_favorites.jpg ├── icon_featured.jpg ├── icon_settings.jpg ├── Logo_Overhang_HD.png ├── Logo_Overhang_SD.png ├── Overhang_Logo_HD.png ├── Overhang_Logo_SD.png ├── mm_icon_focus_hd.png ├── mm_icon_focus_sd.png ├── mm_icon_side_hd.png ├── mm_icon_side_sd.png ├── CommunityPlaylists.jpg ├── YourSubscriptions.jpg ├── icon_next_episode.jpg ├── icon_prev_episode.jpg ├── MainMenu_Icon_Side_HD.png ├── MainMenu_Icon_Side_SD.png ├── Overhang_Background_HD.png ├── Overhang_Background_SD.png ├── MainMenu_Icon_CenterFocus_HD.png ├── MainMenu_Icon_CenterFocus_SD.png ├── Overhang_BackgroundSlice_HD.png └── Overhang_BackgroundSlice_SD.png ├── MyVideoBuzzInstaller.exe ├── MyVideoBuzzInstaller.zip ├── exclude.txt ├── version.xml ├── install.bat ├── manifest ├── xml ├── nursery.xml ├── toprated.xml ├── topfav.xml ├── mostdiscussed.xml ├── mostresponded.xml └── topchannels.xml ├── LICENSE ├── README.md └── source ├── appSettings.brs ├── udp.brs ├── appMain.brs ├── oauth.brs ├── generalDlgs.brs ├── uiToolkit.brs ├── search.brs ├── regScreen.brs ├── reddit.brs ├── url.brs ├── generalUtils.brs └── video.brs /Roku.Deploy.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/Roku.Deploy.exe -------------------------------------------------------------------------------- /myvideobuzz.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/myvideobuzz.zip -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | source/.DS_Store 3 | images/.DS_Store 4 | $tf 5 | myvideobuzz.zip.exclude -------------------------------------------------------------------------------- /images/About.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/About.jpg -------------------------------------------------------------------------------- /images/Search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/Search.jpg -------------------------------------------------------------------------------- /images/icon_s.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_s.jpg -------------------------------------------------------------------------------- /images/History.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/History.jpg -------------------------------------------------------------------------------- /images/Settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/Settings.jpg -------------------------------------------------------------------------------- /images/TopRated.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/TopRated.jpg -------------------------------------------------------------------------------- /images/icon_key.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_key.jpg -------------------------------------------------------------------------------- /images/icon_next.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_next.jpg -------------------------------------------------------------------------------- /images/icon_prev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_prev.jpg -------------------------------------------------------------------------------- /images/icon_user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_user.jpg -------------------------------------------------------------------------------- /images/TopChannels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/TopChannels.jpg -------------------------------------------------------------------------------- /images/icon_search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_search.jpg -------------------------------------------------------------------------------- /images/icon_video.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_video.jpg -------------------------------------------------------------------------------- /images/whattowatch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/whattowatch.jpg -------------------------------------------------------------------------------- /MyVideoBuzzInstaller.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/MyVideoBuzzInstaller.exe -------------------------------------------------------------------------------- /MyVideoBuzzInstaller.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/MyVideoBuzzInstaller.zip -------------------------------------------------------------------------------- /images/ClearHistory.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/ClearHistory.jpg -------------------------------------------------------------------------------- /images/IndianChannels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/IndianChannels.jpg -------------------------------------------------------------------------------- /images/MostDiscussed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/MostDiscussed.jpg -------------------------------------------------------------------------------- /images/MostResponded.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/MostResponded.jpg -------------------------------------------------------------------------------- /images/NurseryRhymes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/NurseryRhymes.jpg -------------------------------------------------------------------------------- /images/TopFavorites.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/TopFavorites.jpg -------------------------------------------------------------------------------- /images/YourFavorites.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/YourFavorites.jpg -------------------------------------------------------------------------------- /images/YourPlaylists.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/YourPlaylists.jpg -------------------------------------------------------------------------------- /images/icon_barcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_barcode.jpg -------------------------------------------------------------------------------- /images/icon_favorites.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_favorites.jpg -------------------------------------------------------------------------------- /images/icon_featured.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_featured.jpg -------------------------------------------------------------------------------- /images/icon_settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_settings.jpg -------------------------------------------------------------------------------- /images/Logo_Overhang_HD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/Logo_Overhang_HD.png -------------------------------------------------------------------------------- /images/Logo_Overhang_SD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/Logo_Overhang_SD.png -------------------------------------------------------------------------------- /images/Overhang_Logo_HD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/Overhang_Logo_HD.png -------------------------------------------------------------------------------- /images/Overhang_Logo_SD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/Overhang_Logo_SD.png -------------------------------------------------------------------------------- /images/mm_icon_focus_hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/mm_icon_focus_hd.png -------------------------------------------------------------------------------- /images/mm_icon_focus_sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/mm_icon_focus_sd.png -------------------------------------------------------------------------------- /images/mm_icon_side_hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/mm_icon_side_hd.png -------------------------------------------------------------------------------- /images/mm_icon_side_sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/mm_icon_side_sd.png -------------------------------------------------------------------------------- /images/CommunityPlaylists.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/CommunityPlaylists.jpg -------------------------------------------------------------------------------- /images/YourSubscriptions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/YourSubscriptions.jpg -------------------------------------------------------------------------------- /images/icon_next_episode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_next_episode.jpg -------------------------------------------------------------------------------- /images/icon_prev_episode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/icon_prev_episode.jpg -------------------------------------------------------------------------------- /images/MainMenu_Icon_Side_HD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/MainMenu_Icon_Side_HD.png -------------------------------------------------------------------------------- /images/MainMenu_Icon_Side_SD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/MainMenu_Icon_Side_SD.png -------------------------------------------------------------------------------- /images/Overhang_Background_HD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/Overhang_Background_HD.png -------------------------------------------------------------------------------- /images/Overhang_Background_SD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/Overhang_Background_SD.png -------------------------------------------------------------------------------- /images/MainMenu_Icon_CenterFocus_HD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/MainMenu_Icon_CenterFocus_HD.png -------------------------------------------------------------------------------- /images/MainMenu_Icon_CenterFocus_SD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/MainMenu_Icon_CenterFocus_SD.png -------------------------------------------------------------------------------- /images/Overhang_BackgroundSlice_HD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/Overhang_BackgroundSlice_HD.png -------------------------------------------------------------------------------- /images/Overhang_BackgroundSlice_SD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utmostsolutions/myvideobuzz/HEAD/images/Overhang_BackgroundSlice_SD.png -------------------------------------------------------------------------------- /exclude.txt: -------------------------------------------------------------------------------- 1 | LICENSE 2 | Makefile 3 | README.md 4 | version.xml 5 | myvideobuzz.zip 6 | myvideobuzz.zip.exclude 7 | MyVideoBuzzInstaller.zip 8 | install.bat 9 | Roku.Deploy.exe 10 | .git 11 | .gitignore 12 | exclude.txt 13 | $tf -------------------------------------------------------------------------------- /version.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.1.0.0 4 | http://utmostsolutions.github.io/myvideobuzz/ 5 | http://github.com/utmostsolutions/myvideobuzz/raw/master/myvideobuzz.zip 6 | 11/30/2013 7 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | 2 | set CURDIR=%~dp0 3 | 4 | del /F /Q %CURDIR%\myvideobuzz.zip 5 | 6 | "C:\Program Files\7-Zip\7z.exe" a -r -tzip -xr@exclude.txt %CURDIR%\myvideobuzz.zip %CURDIR%\* 7 | 8 | Roku.Deploy.exe "%CURDIR%\myvideobuzz.zip" "http://192.168.1.9/plugin_install" "rokudev" "abcd" 9 | 10 | 11 | -------------------------------------------------------------------------------- /manifest: -------------------------------------------------------------------------------- 1 | title=MyVideoBuzz 2 | subtitle=One stop for Internet Videos 3 | mm_icon_focus_hd=pkg:/images/MainMenu_Icon_CenterFocus_HD.png 4 | mm_icon_side_hd=pkg:/images/MainMenu_Icon_Side_HD.png 5 | mm_icon_focus_sd=pkg:/images/MainMenu_Icon_CenterFocus_SD.png 6 | mm_icon_side_sd=pkg:/images/MainMenu_Icon_Side_SD.png 7 | major_version=5 8 | minor_version=2 9 | build_version=00020 -------------------------------------------------------------------------------- /xml/nursery.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | VideoBuzz Top Videos 4 | 5 | 6 | 7 | Mickey Mouse 8 | 9 | 10 | -------------------------------------------------------------------------------- /xml/toprated.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Today 4 | 5 | 6 | 7 | This Week 8 | 9 | 10 | 11 | This Month 12 | 13 | 14 | 15 | All Time 16 | 17 | 18 | -------------------------------------------------------------------------------- /xml/topfav.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Today 4 | 5 | 6 | 7 | This Week 8 | 9 | 10 | 11 | This Month 12 | 13 | 14 | 15 | All Time 16 | 17 | 18 | -------------------------------------------------------------------------------- /xml/mostdiscussed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Today 4 | 5 | 6 | 7 | This Week 8 | 9 | 10 | 11 | This Month 12 | 13 | 14 | 15 | All Time 16 | 17 | 18 | -------------------------------------------------------------------------------- /xml/mostresponded.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Today 4 | 5 | 6 | 7 | This Week 8 | 9 | 10 | 11 | This Month 12 | 13 | 14 | 15 | All Time 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 http://www.myvideobuzz.in/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MyVideoBuzz 2 | ============= 3 | 4 | This project is fork of https://github.com/jesstech/Roku-YouTube, it is updated to fix the API changes and added new features and removed OAuth settings. 5 | 6 | 7 | Installation 8 | ============ 9 | 10 | Enable development mode on your Roku Streaming Player with the following remote 11 | control sequence: 12 | 13 | Home 3x, Up 2x, Right, Left, Right, Left, Right 14 | 15 | When devleopment mode is enabled on your Roku, you can install dev packages 16 | from the Application Installer which runs on your device at your device's IP 17 | address. Open up a standard web browser and visit the following URL: 18 | 19 | http:// (for example, http://192.168.1.6) 20 | 21 | [Download the source as a zip](https://github.com/utmostsolutions/myvideobuzz/raw/master/myvideobuzz.zip) and upload it to your Roku device. 22 | 23 | Due to limitations in the sandboxing of development Roku channels, you can only 24 | have one development channel installed at a time. 25 | 26 | Advanced 27 | ======== 28 | 29 | ### Debugging 30 | 31 | Your Roku's debug console can be accessed by telnet at port 8085: 32 | 33 | telnet 8085 34 | 35 | ### Building from source 36 | 37 | The [Roku Developer SDK](http://www.roku.com/developer) includes a handy Make script 38 | for automatically zipping and installing the channel onto your device should you make 39 | any changes. Just add the project to your SDK's `examples/source` folder and run the 40 | `make install` command from that directory via your terminal. 41 | 42 | 43 | Contributing 44 | ------------ 45 | 46 | Want to contribute? Great! Please contact us http://www.myvideobuzz.in/ -------------------------------------------------------------------------------- /xml/topchannels.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Smosh 4 | 5 | 6 | 7 | RayWilliamJohnson 8 | 9 | 10 | 11 | Jenna Marbles 12 | 13 | 14 | 15 | nigahiga 16 | 17 | 18 | 19 | RihannaVEVO 20 | 21 | 22 | 23 | machinima 24 | 25 | 26 | 27 | PewDiePie 28 | 29 | 30 | 31 | OneDirectionVEVO 32 | 33 | 34 | 35 | HolaSoyGerman 36 | 37 | 38 | 39 | freddiew 40 | 41 | 42 | 43 | TheEllenShow 44 | 45 | 46 | 47 | College Humor 48 | 49 | 50 | 51 | ShaneDawsonTV 52 | 53 | 54 | 55 | EpicMealTime 56 | 57 | 58 | -------------------------------------------------------------------------------- /source/appSettings.brs: -------------------------------------------------------------------------------- 1 | 2 | Sub youtube_browse_settings() 3 | screen = uitkPreShowPosterMenu("","Settings") 4 | settingmenu = [ 5 | { 6 | ShortDescriptionLine1:"Add Account", 7 | ShortDescriptionLine2:"Add your YouTube account", 8 | HDPosterUrl:"pkg:/images/icon_key.jpg", 9 | SDPosterUrl:"pkg:/images/icon_key.jpg" 10 | }, 11 | { 12 | ShortDescriptionLine1:"Clear History", 13 | ShortDescriptionLine2:"Clear your Video History", 14 | HDPosterUrl:"pkg:/images/ClearHistory.jpg", 15 | SDPosterUrl:"pkg:/images/ClearHistory.jpg" 16 | }, 17 | { 18 | ShortDescriptionLine1:"About", 19 | ShortDescriptionLine2:"About the channel", 20 | HDPosterUrl:"pkg:/images/icon_barcode.jpg", 21 | SDPosterUrl:"pkg:/images/icon_barcode.jpg" 22 | } 23 | ] 24 | onselect = [0, m, "AddAccount","ClearHistory","About"] 25 | 26 | uitkDoPosterMenu(settingmenu, screen, onselect) 27 | End Sub 28 | 29 | Sub youtube_add_account() 30 | screen = CreateObject("roKeyboardScreen") 31 | port = CreateObject("roMessagePort") 32 | screen.SetMessagePort(port) 33 | screen.SetTitle("YouTube User Settings") 34 | 35 | ytusername = RegRead("YTUSERNAME1") 36 | if (ytusername <> invalid) then 37 | screen.SetText(ytusername) 38 | end if 39 | 40 | screen.SetDisplayText("Enter your YouTube User name (not email address)") 41 | screen.SetMaxLength(35) 42 | screen.AddButton(1, "Finished") 43 | screen.AddButton(2, "Help") 44 | screen.Show() 45 | 46 | while (true) 47 | msg = wait(0, screen.GetMessagePort()) 48 | if (type(msg) = "roKeyboardScreenEvent") then 49 | if (msg.isScreenClosed()) then 50 | return 51 | else if (msg.isButtonPressed()) then 52 | if (msg.GetIndex() = 1) then 53 | searchText = screen.GetText() 54 | plxml = GetFeedXML("http://gdata.youtube.com/feeds/api/users/" + searchText + "/playlists?v=2&max-results=50") 55 | if (plxml = invalid) then 56 | ShowDialog1Button("Error", searchText + " is not a valid YouTube User Id. Please go to http://utmostsolutions.github.io/myvideobuzz/ to find your YouTube username.", "Ok") 57 | else 58 | RegWrite("YTUSERNAME1", searchText) 59 | screen.Close() 60 | ShowHomeScreen() 61 | return 62 | end if 63 | else 64 | ShowDialog1Button("Help", "Go to http://utmostsolutions.github.io/myvideobuzz/ to find your YouTube username.", "Ok") 65 | end if 66 | end if 67 | end if 68 | end while 69 | End Sub 70 | 71 | 72 | Sub youtube_about() 73 | port = CreateObject("roMessagePort") 74 | screen = CreateObject("roParagraphScreen") 75 | screen.SetMessagePort(port) 76 | 77 | screen.AddHeaderText("About the channel") 78 | screen.AddParagraph("The channel is an open source channel developed by Utmost Solutions, based on the Roku Youtube Channel by Jeston Tigchon. Source code of the channel can be found at http://utmostsolutions.github.io/myvideobuzz/. This channel is not affiliated with Google or YouTube.") 79 | screen.AddParagraph("Version 5.1") 80 | screen.AddButton(1, "Back") 81 | screen.Show() 82 | 83 | while (true) 84 | msg = wait(0, screen.GetMessagePort()) 85 | 86 | if (type(msg) = "roParagraphScreenEvent") then 87 | return 88 | end if 89 | end while 90 | End Sub 91 | 92 | Sub youtube_clear_history() 93 | RegDelete("videos", "history") 94 | ShowErrorDialog("Your video history is deleted", "Clear History") 95 | End Sub 96 | 97 | Function GetFeedXML(plurl As String) As Dynamic 98 | http = NewHttp(plurl) 99 | plrsp = http.GetToStringWithRetry() 100 | 101 | plxml=CreateObject("roXMLElement") 102 | if (not(plxml.Parse(plrsp))) then 103 | return invalid 104 | end if 105 | 106 | if (plxml.GetName() <> "feed") then 107 | return invalid 108 | end if 109 | 110 | if (not(islist(plxml.GetBody()))) then 111 | return invalid 112 | end if 113 | return plxml 114 | End Function -------------------------------------------------------------------------------- /source/udp.brs: -------------------------------------------------------------------------------- 1 | '******************************************************************** 2 | ' Initializes the UDP objects for use in the application. 3 | ' @param youtube the current youtube object 4 | '******************************************************************** 5 | Sub MulticastInit(youtube as Object) 6 | msgPort = createobject("roMessagePort") 7 | udp = createobject("roDatagramSocket") 8 | udp.setMessagePort(msgPort) 'notifications for udp come to msgPort 9 | addr = createobject("roSocketAddress") 10 | addr.setPort(6789) 11 | addr.SetHostName("224.0.0.115") 12 | udp.setAddress(addr) 13 | if (not(udp.setSendToAddress(addr))) then 14 | print ("Failed to set send to address") 15 | return 16 | end if 17 | ' Only local subnet 18 | udp.SetMulticastTTL(1) 19 | if (not(udp.SetMulticastLoop(false))) then 20 | print("Failed to disable multicast loop") 21 | end if 22 | ' Join the multicast group 23 | udp.joinGroup(addr) 24 | udp.NotifyReadable(true) 25 | udp.NotifyWritable(false) 26 | 27 | youtube.udp_socket = udp 28 | youtube.mp_socket = msgPort 29 | End Sub 30 | 31 | '******************************************************************** 32 | ' Determines if someone on the network has tried to query for other videos on the LAN 33 | ' Listens for active video queries, and responds if necessary 34 | '******************************************************************** 35 | Sub CheckForMCast() 36 | regexNewline = CreateObject( "roRegex", "\n", "ig" ) 37 | youtube = LoadYouTube() 38 | if (youtube.mp_socket = invalid OR youtube.udp_socket = invalid) then 39 | print("CheckForMCast: Invalid Message Port or UDP Socket") 40 | return 41 | end if 42 | 43 | message = youtube.mp_socket.GetMessage() 44 | ' Flag to track if a response is necessary -- we only want to respond once, 45 | ' even if we find multiple queries available on the socket 46 | mvbRespond = false 47 | while (message <> invalid) 48 | if (type(message) = "roSocketEvent") then 49 | data = youtube.udp_socket.receiveStr(4096) ' max 4096 characters 50 | 51 | ' Replace newlines 52 | data = regexNewline.ReplaceAll( data, "" ) 53 | ' print("Received " + Left(data, 2) + " from " + Mid(data, 3)) 54 | if ((Left(data, 2) = "1?") AND (Mid(data, 3) <> youtube.device_id)) then 55 | ' Nothing to do if there's no video to watch 56 | if (youtube.activeVideo <> invalid) then 57 | mvbRespond = true 58 | end if 59 | else if ((Left(data, 2) = "2:")) then ' Allow push of videos from other sources on the LAN (not implemented within this source) 60 | ' print("Received force: " + Mid(data, 3)) 61 | youtube.activeVideo = ParseJson(Mid(data, 3)) 62 | else if ((Left(data, 2) = "1:")) then 63 | ' print("Received udp response: " + Mid(data, 3)) 64 | end if 65 | end if 66 | ' This effectively drains the receive queue 67 | message = wait(10, youtube.mp_socket) 68 | end while 69 | if (mvbRespond = true) then 70 | ' Cache the video's XML since we don't want it in the JSON 71 | xml = youtube.activeVideo.xml 72 | ' Zero out the xml prior to conversion to JSON 73 | youtube.activeVideo.xml = invalid 74 | json = SimpleJSONBuilder(youtube.activeVideo) 75 | if (json <> invalid) then 76 | ' Replace all newlines in the JSON 77 | json = regexNewline.ReplaceAll(json, "") 78 | youtube.udp_socket.SendStr("1:" + json) 79 | end if 80 | ' PrintAA(youtube.activeVideo) 81 | ' print(SimpleJSONBuilder(youtube.activeVideo)) 82 | youtube.activeVideo.xml = xml 83 | end if 84 | End Sub 85 | 86 | '******************************************************************** 87 | ' Determines if there are available videos on the LAN to continue watching 88 | ' Multicasts a query for other listening devices to respond with their currently-active video 89 | ' This function is a callback handler for the main menu 90 | ' @param youtube the current youtube object 91 | '******************************************************************** 92 | Sub CheckForLANVideos(youtube as Object) 93 | regexNewline = CreateObject( "roRegex", "\n", "ig" ) 94 | jsonMetadata = [] 95 | if (youtube.mp_socket = invalid OR youtube.udp_socket = invalid) then 96 | print("CheckForMCast: Invalid Message Port or UDP Socket") 97 | return 98 | end if 99 | dialog = ShowPleaseWait("Searching for videos on your LAN") 100 | ' Multicast query 101 | youtube.udp_socket.SendStr("1?" + youtube.device_id) 102 | ' Wait a maximum of 5 seconds for a response 103 | t = CreateObject("roTimespan") 104 | message = wait(2500, youtube.mp_socket) 105 | while (message <> invalid OR t.TotalSeconds() < 5) 106 | if (type(message) = "roSocketEvent") then 107 | data = youtube.udp_socket.receiveStr(4096) ' max 4096 characters 108 | ' print("Received " + Left(data, 2) + " from " + Mid(data, 3)) 109 | ' Replace newlines -- this WILL screw up JSON parsing 110 | data = regexNewline.ReplaceAll( data, "" ) 111 | if ((Left(data, 2) = "1:")) then 112 | response = Mid(data, 3) 113 | ' print("Received udp response: " + response) 114 | jsonObj = ParseJson(response) 115 | if (jsonObj <> invalid) then 116 | jsonMetadata.Push(jsonObj) 117 | end if 118 | end if 119 | ' else the message is invalid 120 | end if 121 | ' If we continue to receive valid roSocketEvent messages, we still want to limit the query to 5 seconds 122 | if (t.TotalSeconds() > 5 OR jsonMetadata.Count() > 50) then 123 | exit while 124 | end if 125 | message = wait(100, youtube.mp_socket) 126 | end while 127 | print("Found " + tostr(jsonMetadata.Count()) + " LAN Videos") 128 | dialog.Close() 129 | youtube.DisplayVideoListFromMetadataList(jsonMetadata, "LAN Videos", invalid, invalid, invalid) 130 | End Sub 131 | -------------------------------------------------------------------------------- /source/appMain.brs: -------------------------------------------------------------------------------- 1 | 2 | Sub Init() 3 | 'if m.oa = invalid then m.oa = InitOauth("RokyouTube", "toasterdesigns.net", "Y6GQqc19mQ2Q5Ux4PFxMOUPk", "1.0") 4 | if (m.youtube = invalid) then 5 | m.youtube = InitYouTube() 6 | end if 7 | End Sub 8 | 9 | Sub RunUserInterface() 10 | 'initialize theme attributes like titles, logos and overhang color 11 | initTheme() 12 | ShowHomeScreen() 13 | End Sub 14 | 15 | 16 | Sub ShowHomeScreen() 17 | ' Pop up start of UI for some instant feedback while we load the icon data 18 | ytusername = RegRead("YTUSERNAME1", invalid) 19 | screen = uitkPreShowPosterMenu("flat-category", ytusername) 20 | if (screen = invalid) then 21 | 'print "unexpected error in uitkPreShowPosterMenu" 22 | return 23 | end if 24 | 25 | Init() 26 | ' oa = Oauth() 27 | youtube = LoadYouTube() 28 | 29 | ' if doRegistration() <> 0 then 30 | ' reason = "unknown" 31 | ' if not oa.linked() then reason = "unlinked" 32 | ' print "Main: exit due to error in registration, reason: "; reason 33 | 'exit the app gently so that the screen doesn't flash to black 34 | ' sleep(25) 35 | ' return 36 | 'end if 37 | 38 | menudata=[] 39 | menudata.Push({ShortDescriptionLine1:"Settings", OnClick:"BrowseSettings", ShortDescriptionLine2:"Edit channel settings", HDPosterUrl:"pkg:/images/Settings.jpg", SDPosterUrl:"pkg:/images/Settings.jpg"}) 40 | menudata.Push({ShortDescriptionLine1:"Search", OnClick:"SearchYoutube", ShortDescriptionLine2:"Search for videos", HDPosterUrl:"pkg:/images/Search.jpg", SDPosterUrl:"pkg:/images/Search.jpg"}) 41 | 42 | menudata.Push({ShortDescriptionLine1:"History", OnClick:"ShowHistory", ShortDescriptionLine2:"View your history", HDPosterUrl:"pkg:/images/History.jpg", SDPosterUrl:"pkg:/images/History.jpg"}) 43 | 44 | if (ytusername<>invalid) and (isnonemptystr(ytusername)) then 45 | menudata.Push({ShortDescriptionLine1:"What to Watch", FeedURL:"users/" + ytusername + "/newsubscriptionvideos?v=2&max-results=50", Category:"false", ShortDescriptionLine2:"What's new to watch", HDPosterUrl:"pkg:/images/whattowatch.jpg", SDPosterUrl:"pkg:/images/whattowatch.jpg"}) 46 | menudata.Push({ShortDescriptionLine1:"My Playlists", FeedURL:"users/" + ytusername + "/playlists?v=2&max-results=50", Category:"true", ShortDescriptionLine2:"Browse your Playlists", HDPosterUrl:"pkg:/images/YourPlaylists.jpg", SDPosterUrl:"pkg:/images/YourPlaylists.jpg"}) 47 | menudata.Push({ShortDescriptionLine1:"My Subscriptions", FeedURL:"users/" + ytusername + "/subscriptions?v=2&max-results=50", Category:"true", ShortDescriptionLine2:"Browse your Subscriptions", HDPosterUrl:"pkg:/images/YourSubscriptions.jpg", SDPosterUrl:"pkg:/images/YourSubscriptions.jpg"}) 48 | menudata.Push({ShortDescriptionLine1:"My Favorites", FeedURL:"users/" + ytusername + "/favorites?v=2&max-results=50", Category:"false", ShortDescriptionLine2:"Browse your favorite videos", HDPosterUrl:"pkg:/images/YourFavorites.jpg", SDPosterUrl:"pkg:/images/YourFavorites.jpg"}) 49 | end if 50 | menudata.Push({ShortDescriptionLine1:"Nursery Rhymes", FeedURL:"pkg:/xml/nursery.xml", Category:"true", ShortDescriptionLine2:"Collection of featured Nursery Rhymes", HDPosterUrl:"pkg:/images/NurseryRhymes.jpg", SDPosterUrl:"pkg:/images/NurseryRhymes.jpg"}) 51 | menudata.Push({ShortDescriptionLine1:"Top Channels", FeedURL:"pkg:/xml/topchannels.xml", Category:"true", ShortDescriptionLine2:"Top Channels", HDPosterUrl:"pkg:/images/TopChannels.jpg", SDPosterUrl:"pkg:/images/TopChannels.jpg"}) 52 | menudata.Push({ShortDescriptionLine1:"Top Rated", FeedURL:"pkg:/xml/toprated.xml", Category:"true", ShortDescriptionLine2:"Top Rated videos", HDPosterUrl:"pkg:/images/TopRated.jpg", SDPosterUrl:"pkg:/images/TopRated.jpg"}) 53 | menudata.Push({ShortDescriptionLine1:"Most Discussed", FeedURL:"pkg:/xml/mostdiscussed.xml", Category:"true", ShortDescriptionLine2:"Most Discussed videos", HDPosterUrl:"pkg:/images/MostDiscussed.jpg", SDPosterUrl:"pkg:/images/MostDiscussed.jpg"}) 54 | menudata.Push({ShortDescriptionLine1:"Top Favorites", FeedURL:"pkg:/xml/topfav.xml", Category:"true", ShortDescriptionLine2:"Top Favorites videos", HDPosterUrl:"pkg:/images/TopFavorites.jpg", SDPosterUrl:"pkg:/images/TopFavorites.jpg"}) 55 | menudata.Push({ShortDescriptionLine1:"Most Responded", FeedURL:"pkg:/xml/mostresponded.xml", Category:"true", ShortDescriptionLine2:"Most Responded videos", HDPosterUrl:"pkg:/images/MostResponded.jpg", SDPosterUrl:"pkg:/images/MostResponded.jpg"}) 56 | 57 | onselect = [1, menudata, m.youtube, 58 | function(menu, youtube, set_idx) 59 | 'PrintAny(0, "menu:", menu) 60 | if (menu[set_idx]["FeedURL"] <> invalid) then 61 | feedurl = menu[set_idx]["FeedURL"] 62 | youtube.FetchVideoList(feedurl,menu[set_idx]["ShortDescriptionLine1"], invalid, strtobool(menu[set_idx]["Category"])) 63 | else if (menu[set_idx]["OnClick"] <> invalid) then 64 | onclickevent = menu[set_idx]["OnClick"] 65 | youtube[onclickevent]() 66 | else if (menu[set_idx]["Custom"] = true) then 67 | menu[set_idx]["ViewFunc"](youtube) 68 | end if 69 | end function] 70 | MulticastInit(youtube) 71 | uitkDoPosterMenu(menudata, screen, onselect) 72 | 73 | sleep(25) 74 | End Sub 75 | 76 | '************************************************************* 77 | '** Set the configurable theme attributes for the application 78 | '** 79 | '** Configure the custom overhang and Logo attributes 80 | '************************************************************* 81 | 82 | Sub initTheme() 83 | app = CreateObject("roAppManager") 84 | theme = CreateObject("roAssociativeArray") 85 | theme.OverhangOffsetSD_X = "72" 86 | theme.OverhangOffsetSD_Y = "31" 87 | theme.OverhangSliceSD = "pkg:/images/Overhang_Background_SD.png" 88 | theme.OverhangLogoSD = "pkg:/images/Overhang_Logo_SD.png" 89 | 90 | theme.OverhangOffsetHD_X = "125" 91 | theme.OverhangOffsetHD_Y = "35" 92 | theme.OverhangSliceHD = "pkg:/images/Overhang_Background_HD.png" 93 | theme.OverhangLogoHD = "pkg:/images/Overhang_Logo_HD.png" 94 | 95 | app.SetTheme(theme) 96 | End Sub 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /source/oauth.brs: -------------------------------------------------------------------------------- 1 | 2 | Function Oauth() As Object 3 | return m.oa 4 | End Function 5 | 6 | '********************************************************* 7 | '** 8 | '** Set up an OAuth object 9 | '** 10 | '********************************************************* 11 | Function InitOauth(appname As String, consumerkey As String, sharedsecret As String, version="1.0" As String) As Object 12 | this = CreateObject("roAssociativeArray") 13 | 14 | this.sign = oauth_sign 15 | this.prep = oauth_prep 16 | this.addParams = oauth_add_params 17 | this.getSignature = oauth_get_signature 18 | this.getSignatureBaseString = oauth_get_signature_base_string 19 | 20 | this.getHmac = oauth_get_hmac 21 | this.initHmac = oauth_init_hmac 22 | this.resetHmac = oauth_reset_hmac 23 | 24 | this.section = "Authentication" 25 | this.items = CreateObject("roList") 26 | this.load = loadReg ' from regScreen.brs 27 | this.save = saveReg ' from regScreen.brs 28 | this.erase = eraseReg ' from regScreen.brs 29 | this.linked = definedReg ' from regScreen.brs 30 | this.dump = dumpReg ' from regScreen.brs 31 | 32 | print "appname: ";appname;" consumerkey: ";consumerkey;" sharedsecret: ";sharedsecret;" version: ";version 33 | 34 | this.appname = appname 35 | this.consumerkey = consumerkey 36 | this.sharedsecret = sharedsecret 37 | this.version = version 38 | 39 | this.items.push("authtoken") 40 | this.items.push("authsecret") 41 | 42 | this.timestamp = createobject("rotimespan") 43 | this.datetime = createobject("rodatetime") 44 | 45 | this.unprotectedkeys = ["oauth_consumer_key", "oauth_nonce", "oauth_signature_method", "oauth_timestamp", "oauth_version" ] 46 | this.protectedkeys = ["oauth_consumer_key", "oauth_nonce", "oauth_signature_method", "oauth_timestamp", "oauth_version", "oauth_token"] 47 | this.verifierkeys = ["oauth_consumer_key", "oauth_nonce", "oauth_signature_method", "oauth_timestamp", "oauth_version", "oauth_token", "oauth_verifier"] 48 | 49 | this.load() 50 | 51 | return this 52 | End Function 53 | 54 | 55 | '********************************************************* 56 | '** 57 | '** Initialize message digesters if necessary. 58 | '** 59 | '********************************************************* 60 | Function oauth_init_hmac(key As String) As Dynamic 61 | hmac = CreateObject("roHMAC") 62 | key_byte_array = CreateObject("roByteArray") 63 | key_byte_array.fromAsciiString(key) 64 | if (hmac.setup("sha1", key_byte_array) <> 0) then 65 | hmac = invalid 66 | end if 67 | return hmac 68 | End Function 69 | 70 | Function oauth_get_hmac(protected As Boolean) As Dynamic 71 | if (protected) then 72 | name = "hmacProtected" 73 | else 74 | name = "hmac" 75 | end if 76 | hmac = m[name] 77 | if (hmac = invalid) then 78 | key = URLEncode(m.sharedsecret) + "&" 79 | protectedKey = protected and isnonemptystr(m.authsecret) 80 | if (protectedKey) then 81 | key = key + URLEncode(m.authsecret) 82 | end if 83 | if (not(protected) OR protectedKey) then 84 | hmac = m.initHmac(key) 85 | m[name] = hmac 86 | end if 87 | end if 88 | return hmac 89 | End Function 90 | 91 | Function oauth_reset_hmac() As Dynamic 92 | m.delete("hmacProtected") 93 | m.delete("hmac") 94 | End Function 95 | 96 | 97 | '********************************************************* 98 | '** 99 | '** Add Oauth parameters to the HTTP request. 100 | '** 101 | '********************************************************* 102 | Function oauth_add_params(http As Object) As Void 103 | http.RemoveParam("oauth_signature") 104 | m.datetime.mark() 'so that m.datetime.asSeconds() retrieves the current time 105 | keyvalues = [ m.consumerkey, itostr(rnd(999999999)), "HMAC-SHA1", itostr(m.datetime.asSeconds()), m.version ] 106 | if (http.accessVerifier) then 107 | keyvalues.push(m.authtoken) 108 | keyvalues.push(m.verifier) 109 | http.addallparams(m.verifierkeys, keyvalues, "urlParams") 110 | else if (http.protected) then 111 | keyvalues.push(m.authtoken) 112 | http.addallparams(m.protectedkeys, keyvalues, "urlParams") 113 | else 114 | http.addallparams(m.unprotectedkeys, keyvalues, "urlParams") 115 | end if 116 | End Function 117 | 118 | 119 | '********************************************************* 120 | '** 121 | '** adds appropriate params and signature 122 | '** via callback on the http object 123 | '** allows just-before-send signature 124 | '** so timestamp is correct on previously 125 | '** composed requests 126 | '** 127 | '********************************************************* 128 | Function Oauth_Callback_Prep() 129 | ' called on an http object 130 | Oauth().prep(m) 131 | End Function 132 | 133 | 134 | '********************************************************* 135 | '** 136 | '** adds appropriate params and signature 137 | '** 138 | '********************************************************* 139 | Function oauth_prep(http As Object) 140 | m.addParams(http) 141 | signature = m.getSignature(http) 142 | http.addParam("oauth_signature", signature, "urlParams") 143 | End Function 144 | 145 | 146 | '********************************************************* 147 | '** 148 | '** adds appropriate params and signature 149 | '** 150 | '********************************************************* 151 | Function oauth_sign(http As Object, protected=true As Boolean, accessVerifier=false As Boolean) 152 | http.callbackPrep = Oauth_Callback_Prep ' defer until go() 153 | http.protected = protected 154 | http.accessVerifier = accessVerifier 155 | End Function 156 | 157 | 158 | '********************************************************* 159 | '********************************************************* 160 | Function oauth_get_signature_base_string(httpObj As Object) as String 161 | sig_base_str = URLEncode(UCase(httpObj.method)) 162 | sig_base_str = sig_base_str + "&" + URLEncode(httpObj.base) 163 | 164 | params = httpObj.GetParams("urlParams") 165 | 166 | if (not(params.empty())) then 167 | sig_base_str = sig_base_str + "&" + UrlEncode(params.encode()) 168 | end if 169 | 170 | 'print "oauth signature base string: "; sig_base_str 171 | return sig_base_str 172 | End Function 173 | 174 | 175 | '********************************************************* 176 | '** 177 | '** @returns The Oauth signature which is computed from 178 | '** the HTTP method and HTTP parameters. 179 | '** 180 | '********************************************************* 181 | Function oauth_get_signature(httpObj As Object) as String 182 | hmac = m.getHmac(httpObj.protected) 183 | if (hmac <> invalid) then 184 | sig_base_str = m.getSignatureBaseString(httpObj) 185 | sig_base_byte_array = CreateObject("roByteArray") 186 | sig_base_byte_array.fromAsciiString(sig_base_str) 187 | result = hmac.process(sig_base_byte_array) 188 | return result.toBase64String() 189 | else 190 | print "HMAC setup error" 191 | return "" 192 | end if 193 | End Function 194 | 195 | -------------------------------------------------------------------------------- /source/generalDlgs.brs: -------------------------------------------------------------------------------- 1 | 2 | '****************************************************** 3 | ' Show basic message dialog without buttons 4 | ' Dialog 'ains up until caller releases the returned object 5 | '****************************************************** 6 | Function ShowPleaseWait(title As String, text = "" As String) As Object 7 | if (not(isstr(title))) then 8 | title = "" 9 | end if 10 | if (not(isstr(text))) then 11 | text = "" 12 | end if 13 | 14 | port = CreateObject("roMessagePort") 15 | dialog = invalid 16 | 17 | 'the OneLineDialog renders a single line of text better 18 | 'than the MessageDialog. 19 | if (text = "") then 20 | dialog = CreateObject("roOneLineDialog") 21 | else 22 | dialog = CreateObject("roMessageDialog") 23 | dialog.SetText(text) 24 | end if 25 | 26 | dialog.SetMessagePort(port) 27 | 28 | dialog.SetTitle(title) 29 | dialog.ShowBusyAnimation() 30 | dialog.Show() 31 | return dialog 32 | End Function 33 | 34 | '****************************************************** 35 | 'Retrieve text for connection failed 36 | '****************************************************** 37 | Function GetConnectionFailedText() as String 38 | return "We were unable to connect to the service. Please try again in a few minutes." 39 | End Function 40 | 41 | '****************************************************** 42 | 'Show connection error dialog 43 | ' 44 | 'Parameter: retry t/f - offer retry option 45 | 'Return 0 = retry, 1 = back 46 | '****************************************************** 47 | Function ShowConnectionFailedRetry() as dynamic 48 | Dbg("Connection Failed Retry") 49 | title = "Can't connect to service" 50 | text = GetConnectionFailedText() 51 | return ShowDialog2Buttons(title, text, "Try Again", "Back") 52 | End Function 53 | 54 | '****************************************************** 55 | ' Show connection error dialog with only an OK button 56 | '****************************************************** 57 | Sub ShowConnectionFailed() 58 | Dbg("Connection Failed") 59 | title = "Can't connect to service" 60 | text = GetConnectionFailedText() 61 | ShowErrorDialog(text, title) 62 | End Sub 63 | 64 | '****************************************************** 65 | ' Show error dialog with OK button 66 | '****************************************************** 67 | Sub ShowErrorDialog(text As dynamic, title = invalid as dynamic) 68 | if (not(isstr(text))) then 69 | text = "Unspecified error" 70 | end if 71 | if (not(isstr(title))) then 72 | title = "Error" 73 | end if 74 | ShowDialog1Button(title, text, "Done") 75 | End Sub 76 | 77 | '****************************************************** 78 | ' Show 1 button dialog 79 | ' Return: nothing 80 | '****************************************************** 81 | Sub ShowDialog1Button(title As Dynamic, text As Dynamic, but1 As String, quickReturn = false As Boolean) 82 | if (not(isstr(title))) then 83 | title = "" 84 | end if 85 | if (not(isstr(text))) then 86 | text = "" 87 | end if 88 | 89 | Dbg("DIALOG1: ", title + " - " + text) 90 | 91 | port = CreateObject( "roMessagePort" ) 92 | dialog = CreateObject( "roMessageDialog" ) 93 | dialog.SetMessagePort(port) 94 | 95 | dialog.SetTitle(title) 96 | dialog.SetText(text) 97 | dialog.AddButton(0, but1) 98 | dialog.Show() 99 | 100 | if (quickReturn = true) then 101 | return 102 | end if 103 | 104 | while (true) 105 | dlgMsg = wait(0, dialog.GetMessagePort()) 106 | 107 | if (type(dlgMsg) = "roMessageDialogEvent") then 108 | if (dlgMsg.isScreenClosed()) then 109 | print "Screen closed" 110 | return 111 | else if (dlgMsg.isButtonPressed()) then 112 | 'print "Button pressed: "; dlgMsg.GetIndex(); " " dlgMsg.GetData() 113 | return 114 | end if 115 | end if 116 | end while 117 | End Sub 118 | 119 | '****************************************************** 120 | 'Show 2 button dialog 121 | 'Return: 0=first button or screen closed, 1=second button 122 | '****************************************************** 123 | Function ShowDialog2Buttons(title As dynamic, text As dynamic, but1 As String, but2 As String) As Integer 124 | if (not(isstr(title))) then 125 | title = "" 126 | end if 127 | if (not(isstr(text))) then 128 | text = "" 129 | end if 130 | 131 | Dbg("DIALOG2: ", title + " - " + text) 132 | 133 | port = CreateObject("roMessagePort") 134 | dialog = CreateObject("roMessageDialog") 135 | dialog.SetMessagePort(port) 136 | 137 | dialog.SetTitle(title) 138 | dialog.SetText(text) 139 | dialog.AddButton(0, but1) 140 | dialog.AddButton(1, but2) 141 | dialog.Show() 142 | 143 | while (true) 144 | dlgMsg = wait(0, dialog.GetMessagePort()) 145 | 146 | if (type(dlgMsg) = "roMessageDialogEvent") then 147 | if (dlgMsg.isScreenClosed()) then 148 | 'print "Screen closed" 149 | dialog = invalid 150 | return 0 151 | else if (dlgMsg.isButtonPressed()) then 152 | 'print "Button pressed: "; dlgMsg.GetIndex(); " " dlgMsg.GetData() 153 | dialog = invalid 154 | return dlgMsg.GetIndex() 155 | end if 156 | end if 157 | end while 158 | End Function 159 | 160 | '****************************************************** 161 | 'Get input from the keyboard 162 | '****************************************************** 163 | Function getKeyboardInput(title As String, search_text As String, submit_text="Submit" As String, cancel_text="Cancel" As String) 164 | screen=CreateObject("roKeyboardScreen") 165 | port=CreateObject("roMessagePort") 166 | 167 | screen.SetMessagePort(port) 168 | screen.SetTitle(title) 169 | screen.SetDisplayText(search_text) 170 | screen.AddButton(1, submit_text) 171 | screen.AddButton(2, cancel_text) 172 | screen.Show() 173 | 174 | while (true) 175 | msg = wait(0, screen.GetMessagePort()) 176 | 177 | if (type(msg) = "roKeyboardScreenEvent") then 178 | if (msg.isScreenClosed()) then 179 | return invalid 180 | else if (msg.isButtonPressed()) then 181 | if (msg.GetIndex() = 1) then 182 | inputText = screen.GetText() 183 | return inputText 184 | else 185 | return invalid 186 | end if 187 | end if 188 | end if 189 | end while 190 | End Function 191 | 192 | '****************************************************** 193 | 'Show basic message dialog without buttons 194 | 'Dialog 'ains up until caller releases the returned object 195 | '****************************************************** 196 | Function ShowDialogNoButton(title As dynamic, text As dynamic) As Object 197 | if (not(isstr(title))) then 198 | title = "" 199 | end if 200 | if (not(isstr(text))) then 201 | text = "" 202 | end if 203 | 204 | port = CreateObject("roMessagePort") 205 | dialog = invalid 206 | 207 | 'the OneLineDialog renders a single line of text better 208 | 'than the MessageDialog. 209 | if (text = "") then 210 | dialog = CreateObject("roOneLineDialog") 211 | else 212 | dialog = CreateObject("roMessageDialog") 213 | dialog.SetText(text) 214 | end if 215 | 216 | dialog.SetMessagePort(port) 217 | 218 | dialog.SetTitle(title) 219 | 'dialog.ShowBusyAnimation() 220 | dialog.Show() 221 | return dialog 222 | End Function -------------------------------------------------------------------------------- /source/uiToolkit.brs: -------------------------------------------------------------------------------- 1 | 2 | ' uitkDoPosterMenu 3 | ' 4 | ' Display "menu" items in a Poster Screen. 5 | ' 6 | Function uitkPreShowPosterMenu(ListStyle="flat-category" as String, breadA = "Home", breadB = invalid) As Object 7 | port=CreateObject("roMessagePort") 8 | screen = CreateObject("roPosterScreen") 9 | screen.SetMessagePort(port) 10 | 11 | if (breadA <> invalid and breadB <> invalid) then 12 | screen.SetBreadcrumbText(breadA, breadB) 13 | else if (breadA <> invalid and breadB = invalid) then 14 | screen.SetBreadcrumbText(breadA, "") 15 | screen.SetTitle(breadA) 16 | end if 17 | 18 | if (ListStyle = "" OR ListStyle = invalid) then 19 | ListStyle = "flat-category" 20 | end if 21 | 22 | screen.SetListStyle(ListStyle) 23 | screen.SetListDisplayMode("scale-to-fit") 24 | ' screen.SetListDisplayMode("zoom-to-fill") 25 | screen.Show() 26 | 27 | return screen 28 | end function 29 | 30 | 31 | Function uitkDoPosterMenu(posterdata, screen, onselect_callback = invalid, onplay_func = invalid) As Integer 32 | if (type(screen) <> "roPosterScreen") then 33 | 'print "illegal type/value for screen passed to uitkDoPosterMenu()" 34 | return -1 35 | end if 36 | 37 | screen.SetContentList(posterdata) 38 | idx% = 0 39 | while (true) 40 | msg = wait(2000, screen.GetMessagePort()) 41 | 42 | 'print "uitkDoPosterMenu | msg type = ";type(msg) 43 | if (type(msg) = "roPosterScreenEvent") then 44 | 'print "event.GetType()=";msg.GetType(); " event.GetMessage()= "; msg.GetMessage() 45 | if (msg.isListItemSelected()) then 46 | if (onselect_callback <> invalid) then 47 | selecttype = onselect_callback[0] 48 | if (selecttype = 0) then 49 | this = onselect_callback[1] 50 | selected_callback = onselect_callback[msg.GetIndex() + 2] 51 | if (islist(selected_callback)) then 52 | f = selected_callback[0] 53 | userdata1 = selected_callback[1] 54 | userdata2 = selected_callback[2] 55 | userdata3 = selected_callback[3] 56 | 57 | if (userdata1 = invalid) then 58 | this[f]() 59 | else if (userdata2 = invalid) then 60 | this[f](userdata1) 61 | else if (userdata3 = invalid) then 62 | this[f](userdata1, userdata2) 63 | else 64 | this[f](userdata1, userdata2, userdata3) 65 | end if 66 | else 67 | if (selected_callback = "return") then 68 | return msg.GetIndex() 69 | else 70 | this[selected_callback]() 71 | end if 72 | end if 73 | else if (selecttype = 1) then 74 | userdata1 = onselect_callback[1] 75 | userdata2 = onselect_callback[2] 76 | f = onselect_callback[3] 77 | f(userdata1, userdata2, msg.GetIndex()) 78 | end if 79 | else 80 | return msg.GetIndex() 81 | end if 82 | else if (msg.isScreenClosed()) then 83 | return -1 84 | else if (msg.isListItemFocused()) then 85 | idx% = msg.GetIndex() 86 | else if (msg.isRemoteKeyPressed()) then 87 | ' If the play button is pressed on the video list, and the onplay_func is valid, play the video 88 | if (onplay_func <> invalid AND msg.GetIndex() = 13) then 89 | onplay_func(posterdata[idx%]) 90 | end if 91 | end if 92 | else if (msg = invalid) then 93 | CheckForMCast() 94 | end if 95 | end while 96 | End Function 97 | 98 | 99 | Function uitkPreShowListMenu(breadA=invalid, breadB=invalid) As Object 100 | port = CreateObject("roMessagePort") 101 | screen = CreateObject("roListScreen") 102 | screen.SetMessagePort(port) 103 | if (breadA <> invalid and breadB <> invalid) then 104 | screen.SetBreadcrumbText(breadA, breadB) 105 | end if 106 | 'screen.SetListStyle("flat-category") 107 | 'screen.SetListDisplayMode("best-fit") 108 | 'screen.SetListDisplayMode("zoom-to-fill") 109 | screen.Show() 110 | 111 | return screen 112 | end function 113 | 114 | 115 | Function uitkDoListMenu(posterdata, screen, onselect_callback=invalid) As Integer 116 | 117 | if (type(screen) <> "roListScreen") then 118 | 'print "illegal type/value for screen passed to uitkDoListMenu()" 119 | return -1 120 | end if 121 | 122 | screen.SetContent(posterdata) 123 | 124 | while (true) 125 | msg = wait(0, screen.GetMessagePort()) 126 | 127 | 'print "uitkDoPosterMenu | msg type = ";type(msg) 128 | 129 | if (type(msg) = "roListScreenEvent") then 130 | 'print "event.GetType()=";msg.GetType(); " Event.GetMessage()= "; msg.GetMessage() 131 | if (msg.isListItemSelected()) then 132 | if (onselect_callback <> invalid) then 133 | selecttype = onselect_callback[0] 134 | if (selecttype = 0) then 135 | this = onselect_callback[1] 136 | selected_callback = onselect_callback[msg.GetIndex() + 2] 137 | if (islist(selected_callback)) then 138 | f = selected_callback[0] 139 | userdata1 = selected_callback[1] 140 | userdata2 = selected_callback[2] 141 | userdata3 = selected_callback[3] 142 | 143 | if (userdata1 = invalid) then 144 | this[f]() 145 | else if (userdata2 = invalid) then 146 | this[f](userdata1) 147 | else if (userdata3 = invalid) then 148 | this[f](userdata1, userdata2) 149 | else 150 | this[f](userdata1, userdata2, userdata3) 151 | end if 152 | else 153 | if (selected_callback = "return") then 154 | return msg.GetIndex() 155 | else 156 | this[selected_callback]() 157 | end if 158 | end if 159 | else if (selecttype = 1) then 160 | userdata1=onselect_callback[1] 161 | userdata2=onselect_callback[2] 162 | f=onselect_callback[3] 163 | f(userdata1, userdata2, msg.GetIndex()) 164 | end if 165 | else 166 | return msg.GetIndex() 167 | end if 168 | else if (msg.isScreenClosed()) then 169 | return -1 170 | end if 171 | end if 172 | end while 173 | End Function 174 | 175 | 176 | Function uitkDoCategoryMenu(categoryList, screen, content_callback = invalid, onclick_callback = invalid, onplay_func = invalid) As Integer 177 | 'Set current category to first in list 178 | category_idx = 0 179 | contentlist = [] 180 | 181 | screen.SetListNames(categoryList) 182 | contentdata1 = content_callback[0] 183 | contentdata2 = content_callback[1] 184 | content_f = content_callback[2] 185 | 186 | contentlist = content_f(contentdata1, contentdata2, 0) 187 | 188 | if (contentlist.Count() = 0) then 189 | screen.SetContentList([]) 190 | screen.clearmessage() 191 | screen.showmessage("No viewable content in this section") 192 | else 193 | screen.SetContentList(contentlist) 194 | screen.clearmessage() 195 | end if 196 | screen.Show() 197 | idx% = 0 198 | 199 | while (true) 200 | msg = wait(2000, screen.GetMessagePort()) 201 | if (type(msg) = "roPosterScreenEvent") then 202 | if (msg.isListFocused()) then 203 | category_idx = msg.GetIndex() 204 | contentdata1 = content_callback[0] 205 | contentdata2 = content_callback[1] 206 | content_f = content_callback[2] 207 | 208 | contentlist = content_f(contentdata1, contentdata2, category_idx) 209 | 210 | if (contentlist.Count() = 0) then 211 | screen.SetContentList([]) 212 | screen.ShowMessage("No viewable content in this section") 213 | else 214 | screen.SetContentList(contentlist) 215 | screen.SetFocusedListItem(0) 216 | end if 217 | else if (msg.isListItemSelected()) then 218 | userdata1 = onclick_callback[0] 219 | userdata2 = onclick_callback[1] 220 | content_f = onclick_callback[2] 221 | 222 | contentData = content_f(userdata1, userdata2, contentlist, category_idx, msg.GetIndex()) 223 | if ( contentData.isContentList = true ) then 224 | contentlist = contentData.content 225 | if (contentlist.Count() <> 0) then 226 | if ( contentlist[0]["action"] <> invalid AND contentlist.Count() > 1 ) then 227 | screen.SetFocusedListItem(1) 228 | else 229 | screen.SetFocusedListItem(0) 230 | end if 231 | screen.SetContentList(contentlist) 232 | 'screen.SetFocusedListItem(msg.GetIndex()) 233 | end if 234 | end if 235 | else if (msg.isListItemFocused()) then 236 | idx% = msg.GetIndex() 237 | else if (msg.isScreenClosed()) then 238 | return -1 239 | else if (msg.isRemoteKeyPressed()) then 240 | ' If the play button is pressed on the video list, and the onplay_func is valid, play the video 241 | if (onplay_func <> invalid AND msg.GetIndex() = 13) then 242 | onplay_func(contentlist[idx%]) 243 | end if 244 | end if 245 | else if (msg = invalid) then 246 | CheckForMCast() 247 | end If 248 | end while 249 | End Function 250 | 251 | Sub uitkDoMessage(message, screen) 252 | screen.showMessage(message) 253 | while (true) 254 | msg = wait(0, screen.GetMessagePort()) 255 | if (msg.isScreenClosed()) then 256 | return 257 | end if 258 | end while 259 | End Sub -------------------------------------------------------------------------------- /source/search.brs: -------------------------------------------------------------------------------- 1 | '******************************************************************** 2 | ' YouTube Search 3 | '******************************************************************** 4 | Sub youtube_search() 5 | port = CreateObject("roMessagePort") 6 | screen = CreateObject("roSearchScreen") 7 | screen.SetMessagePort(port) 8 | 9 | history = CreateObject("roSearchHistory") 10 | screen.SetSearchTerms(history.GetAsArray()) 11 | screen.SetBreadcrumbText("", "Hit the * button for search options") 12 | screen.Show() 13 | 14 | while (true) 15 | msg = wait(0, port) 16 | 17 | if (type(msg) = "roSearchScreenEvent") then 18 | 'print "Event: "; msg.GetType(); " msg: "; msg.GetMessage() 19 | if (msg.isScreenClosed()) then 20 | return 21 | else if (msg.isPartialResult()) then 22 | screen.SetSearchTermHeaderText("Suggestions:") 23 | screen.SetClearButtonEnabled(false) 24 | screen.SetSearchTerms(GenerateSearchSuggestions(msg.GetMessage())) 25 | else if (msg.isFullResult()) then 26 | keyword = msg.GetMessage() 27 | query = "videos?q=" + keyword 28 | prompt = "Searching YouTube for " + Quote() + keyword + Quote() 29 | if (m.searchLengthFilter <> "") then 30 | query = query + "&duration=" + LCase(m.searchLengthFilter) 31 | prompt = prompt + Chr(10) + "Length: " + m.searchLengthFilter 32 | end if 33 | if (m.searchDateFilter <> "") then 34 | query = query + "&time=" + strReplace(LCase(m.searchDateFilter), " ", "_") 35 | prompt = prompt + Chr(10) + "Timeframe: " + m.searchDateFilter 36 | end if 37 | if (m.searchSort <> "") then 38 | query = query + "&orderby=" + m.searchSort 39 | prompt = prompt + Chr(10) + "Sort: " + GetSortText(m.searchSort) 40 | end if 41 | dialog = ShowPleaseWait("Please wait", prompt) 42 | xml = m.ExecServerAPI(query, invalid)["xml"] 43 | if (not(isxmlelement(xml))) then 44 | dialog.Close() 45 | ShowConnectionFailed() 46 | return 47 | end if 48 | videos = m.newVideoListFromXML(xml.entry) 49 | if (videos.Count() > 0) then 50 | history.Push(keyword) 51 | screen.AddSearchTerm(keyword) 52 | dialog.Close() 53 | m.DisplayVideoListFromVideoList(videos, "Search Results for " + Chr(39) + keyword + Chr(39), xml.link, invalid, invalid) 54 | else 55 | dialog.Close() 56 | ShowErrorDialog("No videos match your search", "Search results") 57 | end if 58 | else if (msg.isCleared()) then 59 | history.Clear() 60 | else if ((msg.isRemoteKeyPressed() AND msg.GetIndex() = 10) OR msg.isButtonInfo()) then 61 | while (SearchOptionDialog() = 1) 62 | end while 63 | 'else 64 | 'print("Unhandled event on search screen") 65 | end if 66 | 'else 67 | 'print("Unhandled msg type: " + type(msg)) 68 | end if 69 | end while 70 | End Sub 71 | 72 | 73 | Function GenerateSearchSuggestions(partSearchText As String) As Object 74 | suggestions = CreateObject("roArray", 1, true) 75 | length = len(partSearchText) 76 | if (length > 0) then 77 | searchRequest = CreateObject("roUrlTransfer") 78 | searchRequest.SetURL("http://suggestqueries.google.com/complete/search?hl=en&client=youtube&hjson=t&ds=yt&jsonp=window.yt.www.suggest.handleResponse&q=" + URLEncode(partSearchText)) 79 | jsonAsString = searchRequest.GetToString() 80 | jsonAsString = strReplace(jsonAsString,"window.yt.www.suggest.handleResponse(","") 81 | jsonAsString = Left(jsonAsString, Len(jsonAsString) -1) 82 | response = simpleJSONParser(jsonAsString) 83 | 84 | if (islist(response) = true) then 85 | if (response.Count() > 1) then 86 | for each sugg in response[1] 87 | suggestions.Push(sugg[0]) 88 | end for 89 | end if 90 | end if 91 | 92 | else 93 | history = CreateObject("roSearchHistory") 94 | suggestions = history.GetAsArray() 95 | end if 96 | return suggestions 97 | End Function 98 | 99 | Function SearchOptionDialog() as Integer 100 | dialog = CreateObject("roMessageDialog") 101 | port = CreateObject("roMessagePort") 102 | dialog.SetMessagePort(port) 103 | dialog.SetTitle("Search Settings") 104 | updateSearchDialogText(dialog) 105 | dialog.EnableBackButton(true) 106 | dialog.addButton(1, "Change Length Filter") 107 | dialog.addButton(2, "Change Time Filter") 108 | dialog.addButton(3, "Change Sort Setting") 109 | dialog.addButton(4, "Done") 110 | dialog.Show() 111 | while true 112 | dlgMsg = wait(0, dialog.GetMessagePort()) 113 | if (type(dlgMsg) = "roMessageDialogEvent") then 114 | if (dlgMsg.isButtonPressed()) then 115 | if (dlgMsg.GetIndex() = 1) then 116 | dialog.Close() 117 | ret = SearchFilterClicked() 118 | if (ret <> "ignore") then 119 | m.youtube.searchLengthFilter = ret 120 | if (ret <> "") then 121 | RegWrite("length", ret, "Search") 122 | else 123 | RegDelete("length", "Search") 124 | end if 125 | updateSearchDialogText(dialog, true) 126 | end if 127 | return 1 ' Re-open the options 128 | else if (dlgMsg.GetIndex() = 2) then 129 | dialog.Close() 130 | ret = SearchDateClicked() 131 | if (ret <> "ignore") then 132 | m.youtube.searchDateFilter = ret 133 | if (ret <> "") then 134 | RegWrite("date", ret, "Search") 135 | else 136 | RegDelete("date", "Search") 137 | end if 138 | updateSearchDialogText(dialog, true) 139 | end if 140 | return 1 ' Re-open the options 141 | else if (dlgMsg.GetIndex() = 3) then 142 | dialog.Close() 143 | ret = SearchSortClicked() 144 | if (ret <> "ignore") then 145 | m.youtube.searchSort = ret 146 | if (ret <> "") then 147 | RegWrite("sort", ret, "Search") 148 | else 149 | RegDelete("sort", "Search") 150 | end if 151 | updateSearchDialogText(dialog, true) 152 | end if 153 | return 1 ' Re-open the options 154 | else if (dlgMsg.GetIndex() = 4) then 155 | dialog.Close() 156 | exit while 157 | end if 158 | else if (dlgMsg.isScreenClosed()) then 159 | dialog.Close() 160 | exit while 161 | else 162 | ' print ("Unhandled msg type") 163 | exit while 164 | end if 165 | else 166 | ' print ("Unhandled msg: " + type(dlgMsg)) 167 | exit while 168 | end if 169 | end while 170 | ' print ("Exiting search option dialog") 171 | return 0 172 | End Function 173 | 174 | Sub updateSearchDialogText(dialog as Object, isUpdate = false as Boolean) 175 | searchLengthText = "None" 176 | searchDateText = "None" 177 | searchSortText = "None" 178 | if (m.youtube.searchLengthFilter <> "") then 179 | searchLengthText = m.youtube.searchLengthFilter 180 | end if 181 | if (m.youtube.searchDateFilter <> "") then 182 | searchDateText = m.youtube.searchDateFilter 183 | end if 184 | if (m.youtube.searchSort <> "") then 185 | searchSortText = GetSortText(m.youtube.searchSort) 186 | end if 187 | dialogText = "Length: " + searchLengthText + chr(10) + "Timeframe: " + searchDateText + chr(10) + "Sort: " + searchSortText 188 | if (isUpdate = true) then 189 | dialog.UpdateText(dialogText) 190 | else 191 | dialog.SetText(dialogText) 192 | end if 193 | End Sub 194 | 195 | Function SearchFilterClicked() as String 196 | dialog = CreateObject("roMessageDialog") 197 | port = CreateObject("roMessagePort") 198 | dialog.SetMessagePort(port) 199 | dialog.SetTitle("Length Filter") 200 | dialog.EnableBackButton(true) 201 | dialog.addButton(1, "None") 202 | dialog.addButton(2, "Short (<4 minutes)") 203 | dialog.addButton(3, "Medium (>=4 and <=20 minutes)") 204 | dialog.addButton(4, "Long (>20 minutes)") 205 | if (m.youtube.searchLengthFilter = "Short") then 206 | dialog.SetFocusedMenuItem(1) 207 | else if (m.youtube.searchLengthFilter = "Medium") then 208 | dialog.SetFocusedMenuItem(2) 209 | else if (m.youtube.searchLengthFilter = "Long") then 210 | dialog.SetFocusedMenuItem(3) 211 | end if 212 | dialog.Show() 213 | retVal = "ignore" 214 | while true 215 | dlgMsg = wait(0, dialog.GetMessagePort()) 216 | if (type(dlgMsg) = "roMessageDialogEvent") then 217 | if (dlgMsg.isButtonPressed()) then 218 | if (dlgMsg.GetIndex() = 1) then 219 | retVal = "" 220 | else if (dlgMsg.GetIndex() = 2) then 221 | retVal = "Short" 222 | else if (dlgMsg.GetIndex() = 3) then 223 | retVal = "Medium" 224 | else if (dlgMsg.GetIndex() = 4) then 225 | retVal = "Long" 226 | end if 227 | exit while 228 | else if (dlgMsg.isScreenClosed()) then 229 | exit while 230 | end if 231 | end if 232 | end while 233 | dialog.Close() 234 | ' print ("Exiting SearchFilterClicked") 235 | return retVal 236 | End Function 237 | 238 | Function SearchDateClicked() as String 239 | dialog = CreateObject("roMessageDialog") 240 | port = CreateObject("roMessagePort") 241 | dialog.SetMessagePort(port) 242 | dialog.SetTitle("Timeframe Filter") 243 | dialog.EnableBackButton(true) 244 | dialog.addButton(1, "None") 245 | dialog.addButton(2, "Today") 246 | dialog.addButton(3, "This Week") 247 | dialog.addButton(4, "This Month") 248 | if (m.youtube.searchDateFilter = "Today") then 249 | dialog.SetFocusedMenuItem(1) 250 | else if (m.youtube.searchDateFilter = "This Week") then 251 | dialog.SetFocusedMenuItem(2) 252 | else if (m.youtube.searchDateFilter = "This Month") then 253 | dialog.SetFocusedMenuItem(3) 254 | end if 255 | dialog.Show() 256 | retVal = "ignore" 257 | while true 258 | dlgMsg = wait(0, dialog.GetMessagePort()) 259 | if (type(dlgMsg) = "roMessageDialogEvent") then 260 | if (dlgMsg.isButtonPressed()) then 261 | if (dlgMsg.GetIndex() = 1) then 262 | retVal = "" 263 | else if (dlgMsg.GetIndex() = 2) then 264 | retVal = "Today" 265 | else if (dlgMsg.GetIndex() = 3) then 266 | retVal = "This Week" 267 | else if (dlgMsg.GetIndex() = 4) then 268 | retVal = "This Month" 269 | end if 270 | exit while 271 | else if (dlgMsg.isScreenClosed()) then 272 | exit while 273 | end if 274 | end if 275 | end while 276 | dialog.Close() 277 | ' print ("Exiting SearchDateClicked") 278 | return retVal 279 | End Function 280 | 281 | Function SearchSortClicked() as String 282 | dialog = CreateObject("roMessageDialog") 283 | port = CreateObject("roMessagePort") 284 | dialog.SetMessagePort(port) 285 | dialog.SetTitle("Sort Options") 286 | dialog.EnableBackButton(true) 287 | dialog.addButton(1, "None") 288 | dialog.addButton(2, "Newest First") 289 | dialog.addButton(3, "Views (most to least)") 290 | dialog.addButton(4, "Rating (highest to lowest)") 291 | if (m.youtube.searchSort = "published") then 292 | dialog.SetFocusedMenuItem(1) 293 | else if (m.youtube.searchSort = "viewCount") then 294 | dialog.SetFocusedMenuItem(2) 295 | else if (m.youtube.searchSort = "rating") then 296 | dialog.SetFocusedMenuItem(3) 297 | end if 298 | dialog.Show() 299 | retVal = "ignore" 300 | while true 301 | dlgMsg = wait(0, dialog.GetMessagePort()) 302 | if (type(dlgMsg) = "roMessageDialogEvent") then 303 | if (dlgMsg.isButtonPressed()) then 304 | if (dlgMsg.GetIndex() = 1) then 305 | retVal = "" 306 | else if (dlgMsg.GetIndex() = 2) then 307 | retVal = "published" 308 | else if (dlgMsg.GetIndex() = 3) then 309 | retVal = "viewCount" 310 | else if (dlgMsg.GetIndex() = 4) then 311 | retVal = "rating" 312 | end if 313 | exit while 314 | else if (dlgMsg.isScreenClosed()) then 315 | exit while 316 | end if 317 | end if 318 | end while 319 | dialog.Close() 320 | ' print ("Exiting SearchSortClicked") 321 | return retVal 322 | End Function 323 | 324 | Function GetSortText(internalValue as String) as String 325 | retVal = "None" 326 | if (m.youtube.searchSort = "published") then 327 | retVal = "Newest First" 328 | else if (m.youtube.searchSort = "viewCount") then 329 | retVal = "Views" 330 | else if (m.youtube.searchSort = "rating") then 331 | retVal = "Rating" 332 | end if 333 | return retVal 334 | End Function -------------------------------------------------------------------------------- /source/regScreen.brs: -------------------------------------------------------------------------------- 1 | 2 | Function doRegistration() As Integer 3 | 4 | screenFacade = CreateObject("roParagraphScreen") 5 | screenFacade.show() 6 | 7 | oa = Oauth() 8 | 9 | if (oa.linked()) then 10 | token_status = checkOauthToken() 11 | if (token_status = 1) then 12 | oa.erase() 13 | else if (token_status = 2) then 14 | return token_status 15 | end if 16 | end if 17 | 18 | if (not(oa.linked())) then 19 | status = doOauthLink() 20 | if (status <> 0) then 21 | return status 22 | end if 23 | showCongratulationsScreen() 24 | end if 25 | 26 | return 0 27 | 28 | End Function 29 | 30 | Function checkOauthToken() As Integer 31 | print "RegScreen: checkOauthToken" 32 | 33 | youtube = LoadYouTube() 34 | oa = Oauth() 35 | 36 | http = NewHttp(youtube.oauth_prefix+"/AuthSubTokenInfo") 37 | oa.sign(http,true) 38 | http.getToStringWithTimeout(10) 39 | if (http.status = 200) then 40 | return 0 41 | else 42 | ans=ShowDialog2Buttons("Token invalid", "Unable to authenticate. This could be a temporary issue or due to revoked access by the user.", "Link Again", "Exit") 43 | if (ans = 0) then 44 | return 1 45 | else 46 | return 2 47 | end if 48 | end if 49 | End Function 50 | 51 | Function doOauthLink() As Integer 52 | status = doTempLink() 53 | if (status = 0) 54 | status = doYouTubeEnroll() 55 | if (status = 0) then 56 | status = doLink() 57 | end if 58 | end if 59 | 60 | return status 61 | End Function 62 | 63 | Function doTempLink() As Integer 64 | print "RegScreen: doTempLink" 65 | status = 2 66 | 67 | youtube = LoadYouTube() 68 | oa = Oauth() 69 | 70 | http = NewHttp(youtube.oauth_prefix+"/OAuthGetRequestToken") 71 | http.AddParam("scope",youtube.scope) 72 | http.AddParam("oauth_callback",youtube.link_prefix+"/oauth/callback") 73 | oa.sign(http,false) 74 | rsp = http.getToStringWithTimeout(10) 75 | 76 | print "RegScreen: http failure = "; http.Http.GetFailureReason() 77 | print "RegScreen: temporary registration response = "; rsp 78 | 79 | 'temporary token 80 | params = NewUrlParams(rsp) 81 | oa.authtoken = params.get("oauth_token") 82 | oa.authsecret = params.get("oauth_token_secret") 83 | 84 | if (isnonemptystr(oa.authtoken) AND isnonemptystr(oa.authsecret)) then 85 | print "temp oauth: "; oa.dump() 86 | status = 0 87 | else 88 | print "RegScreen: failed to retrieve temporary token" 89 | print "temp oauth: "; oa.dump() 90 | status = 2 91 | end if 92 | 93 | return status 94 | End Function 95 | 96 | Function doYouTubeEnroll() As Integer 97 | print "RegScreen: doYouTubeEnroll" 98 | status = 1 ' error 99 | 100 | youtube = LoadYouTube() 101 | oa = Oauth() 102 | 103 | regscreen = displayRegistrationScreen() 104 | 105 | while (true) 106 | sn = CreateObject("roDeviceInfo").GetDeviceUniqueId() 107 | regCode = getRegistrationCode(sn) 108 | 109 | 'if we've failed to get the registration code, bail out, otherwise we'll 110 | 'get rid of the retreiving... text and replace it with the real code 111 | if (regCode = "") then 112 | return 2 113 | end if 114 | regscreen.SetRegistrationCode(regCode) 115 | print "Enter registration code " + regCode + " at " + youtube.link_prefix + " for " + sn 116 | 117 | duration = 0 118 | 'make an http request to see if the device has been registered on the backend 119 | while (true) 120 | status = checkRegistrationStatus(sn, regCode) 121 | print itostr(status) 122 | if (status < 3) then 123 | return status 124 | end if 125 | 126 | getNewCode = false 127 | retryInterval=m.retryInterval 128 | retryDuration=m.retryDuration 129 | print "retry duration "; itostr(duration); " at "; itostr(retryInterval); 130 | print " sec intervals for "; itostr(retryDuration); " secs max" 131 | 132 | 'wait for the retry interval to expire or the user to press a button 133 | 'indicating they either want to quit or fetch a new registration code 134 | while (true) 135 | print "Wait for " + itostr(retryInterval) 136 | msg = wait(retryInterval * 1000, regscreen.GetMessagePort()) 137 | duration = duration + retryInterval 138 | if (msg = invalid) then 139 | exit while 140 | end if 141 | 142 | if (type(msg) = "roCodeRegistrationScreenEvent") then 143 | if (msg.isScreenClosed()) then 144 | print "Screen closed" 145 | return 1 146 | else if (msg.isButtonPressed()) then 147 | print "Button pressed: "; msg.GetIndex(); " " msg.GetData() 148 | if (msg.GetIndex() = 0) then 149 | regscreen.SetRegistrationCode("retrieving code...") 150 | getNewCode = true 151 | exit while 152 | end if 153 | if (msg.GetIndex() = 1) then 154 | return 1 155 | end if 156 | end if 157 | end if 158 | end while 159 | 160 | if (duration >= retryDuration) then 161 | ans = ShowDialog2Buttons("Request timed out", "Unable to link to YouTube within time limit.", "Try Again", "Back") 162 | if (ans = 0) then 163 | regscreen.SetRegistrationCode("retrieving code...") 164 | getNewCode = true 165 | else 166 | return 1 167 | end if 168 | end if 169 | 170 | if (getNewCode) then 171 | exit while 172 | end if 173 | 174 | print "poll prelink again..." 175 | end while 176 | end while 177 | 178 | print "RegScreen: enroll status: "; status 179 | return status 180 | End Function 181 | 182 | Function doLink() As Integer 183 | print "RegScreen: doLink" 184 | status = 2 185 | 186 | youtube = LoadYouTube() 187 | oa = Oauth() 188 | 189 | http = NewHttp(youtube.oauth_prefix+"/OAuthGetAccessToken") 190 | 'oa.verifier = pinCode 191 | oa.sign(http,true,true) 192 | print "RegScreen: access_token URL: "; http.GetUrl() 193 | 194 | rsp = http.getToStringWithTimeout(10) 195 | print "RegScreen: final registration response = "; rsp 196 | 197 | params = NewUrlParams(rsp) 198 | oa.authtoken = params.get("oauth_token") 199 | oa.authsecret = params.get("oauth_token_secret") 200 | oa.resetHmac() 201 | 202 | if (oa.linked()) then 203 | oa.save() 204 | print "RegScreen: final oauth: "; oa.dump() 205 | status = 0 206 | else 207 | print "RegScreen: failed to retrieve final authorization token" 208 | end if 209 | 210 | return status 211 | End Function 212 | 213 | 214 | '****************************************************** 215 | 'Load/Save a set of parameters to registry 216 | 'These functions must be called from an AA that has 217 | 'a "section" string and an "items" list of strings. 218 | '****************************************************** 219 | Function loadReg() As Boolean 220 | for each item in m.items 221 | temp = RegRead(item, m.section) 222 | if (temp = invalid) then 223 | temp = "" 224 | end if 225 | m[item] = temp 226 | end for 227 | return definedReg() 228 | End Function 229 | 230 | Function saveReg() 231 | for each item in m.items 232 | RegWrite(item, m[item], m.section) 233 | end for 234 | End Function 235 | 236 | Function eraseReg() 237 | for each item in m.items 238 | RegDelete(item, m.section) 239 | m[item] = "" 240 | end for 241 | End Function 242 | 243 | Function definedReg() As Boolean 244 | for each item in m.items 245 | if (not(m.DoesExist(item))) then 246 | return false 247 | end if 248 | if (Len(m[item]) = 0) then 249 | return false 250 | end if 251 | end for 252 | return true 253 | End Function 254 | 255 | Function dumpReg() As String 256 | result = "" 257 | for each item in m.items 258 | if (m.DoesExist(item)) then 259 | result = result + " " + item + "=" + m[item] 260 | end if 261 | end for 262 | return result 263 | End Function 264 | 265 | '******************************************************************** 266 | '** Fetch the prelink code from the registration service. return 267 | '** valid registration code on success or an empty string on failure 268 | '******************************************************************** 269 | Function getRegistrationCode(sn As String) As String 270 | if (sn = "") then 271 | return "" 272 | end if 273 | 274 | oa = Oauth() 275 | youtube = LoadYouTube() 276 | 277 | http = NewHttp(youtube.link_prefix+"/getRegCode?partner=roku&service=youtube&deviceTypeName=roku&deviceID="+sn+"&oauth_token="+oa.authtoken) 278 | print "RegScreen: access_token URL: "; http.GetUrl() 279 | 280 | rsp = http.getToStringWithTimeout(10) 281 | 282 | xml=ParseXML(rsp) 283 | print "GOT: " + rsp 284 | print "Reason: " + http.Http.GetFailureReason() 285 | 286 | if (xml = invalid) then 287 | print "Can't parse getRegistrationCode response" 288 | ShowConnectionFailed() 289 | return "" 290 | end if 291 | 292 | if (xml.GetName() <> "result") then 293 | Dbg("Bad register response: ", xml.GetName()) 294 | ShowConnectionFailed() 295 | return "" 296 | end if 297 | 298 | if (islist(xml.GetBody()) = false) then 299 | Dbg("No registration information available") 300 | ShowConnectionFailed() 301 | return "" 302 | end if 303 | 304 | 'default values for retry logic 305 | retryInterval = 30 'seconds 306 | retryDuration = 900 'seconds (aka 15 minutes) 307 | regCode = "" 308 | 309 | 'handle validation of response fields 310 | for each e in xml.GetBody() 311 | if (e.GetName() = "regCode") then 312 | regCode = e.GetBody() 'enter this code at website 313 | else if (e.GetName() = "retryInterval") then 314 | retryInterval = strtoi(e.GetBody()) 315 | else if (e.GetName() = "retryDuration") then 316 | retryDuration = strtoi(e.GetBody()) 317 | end if 318 | next 319 | 320 | if (regCode = "") then 321 | Dbg("Parse yields empty registration code") 322 | ShowConnectionFailed() 323 | end if 324 | 325 | m.retryDuration = retryDuration 326 | m.retryInterval = retryInterval 327 | m.regCode = regCode 328 | 329 | return regCode 330 | End Function 331 | 332 | Function displayRegistrationScreen() As Object 333 | youtube = LoadYouTube() 334 | 335 | regsite = youtube.link_prefix 336 | regscreen = CreateObject("roCodeRegistrationScreen") 337 | regscreen.SetMessagePort(CreateObject("roMessagePort")) 338 | 339 | regscreen.SetTitle("") 340 | regscreen.AddParagraph("Please link your Roku player to your YouTube account") 341 | regscreen.AddFocalText(" ", "spacing-dense") 342 | regscreen.AddFocalText("From your computer, go to", "spacing-dense") 343 | regscreen.AddFocalText(regsite, "spacing–dense") 344 | regscreen.AddFocalText("and enter this code to activate:", "spacing-dense") 345 | regscreen.SetRegistrationCode("retrieving code...") 346 | regscreen.AddParagraph("This screen will automatically update as soon as your activation completes") 347 | regscreen.AddButton(0, "Get a new code") 348 | regscreen.AddButton(1, "Back") 349 | regscreen.Show() 350 | 351 | return regscreen 352 | End Function 353 | 354 | '****************************************************************** 355 | '** Check the status of the registration to see if we've linked 356 | '** Returns: 357 | '** 0 - We're registered. Proceed. 358 | '** 1 - Reserved. Used by calling function. 359 | '** 2 - We're not registered. There was an error, abort. 360 | '** 3 - We're not registered. Keep trying. 361 | '****************************************************************** 362 | Function checkRegistrationStatus(sn As String, regCode As String) As Integer 363 | oa = Oauth() 364 | youtube = LoadYouTube() 365 | 366 | print "checking registration status" 367 | http = NewHttp(youtube.link_prefix+"/getRegResult?service=youtube&partner=roku&deviceID="+sn+"®Code="+regCode) 368 | 369 | while (true) 370 | rsp = http.getToStringWithTimeout(10) 371 | print rsp 372 | xml=ParseXML(rsp) 373 | if (xml = invalid) then 374 | print "Can't parse check registration status response" 375 | ShowConnectionFailed() 376 | return 2 377 | end if 378 | 379 | if (xml.GetName() <> "result") then 380 | print "unexpected check registration status response: ", xml.GetName() 381 | ShowConnectionFailed() 382 | return 2 383 | end if 384 | 385 | if (islist(xml.GetBody()) = true) then 386 | for each e in xml.GetBody() 387 | if (e.GetName() = "status") then 388 | status = e.GetBody() 389 | 390 | if (status = "failure") then 391 | ShowConnectionFailed() 392 | return 2 393 | else if (status = "incomplete") then 394 | return 3 395 | end if 396 | else if (e.GetName() = "oauth_verifier") then 397 | print "got oauth_verifier: "+e.GetBody() 398 | oa.verifier = e.GetBody() 399 | return 0 400 | end if 401 | next 402 | end if 403 | end while 404 | End Function 405 | 406 | '****************************************************** 407 | 'Show congratulations screen 408 | '****************************************************** 409 | Sub showCongratulationsScreen() 410 | port = CreateObject("roMessagePort") 411 | screen = CreateObject("roParagraphScreen") 412 | screen.SetMessagePort(port) 413 | 414 | screen.AddHeaderText("Congratulations!") 415 | screen.AddParagraph("You have successfully linked your Roku player to your YouTube account.") 416 | screen.AddParagraph("Select 'start' to begin.") 417 | screen.AddButton(1, "start") 418 | screen.Show() 419 | 420 | while (true) 421 | msg = wait(0, screen.GetMessagePort()) 422 | 423 | if (type(msg) = "roParagraphScreenEvent") then 424 | if (msg.isScreenClosed()) then 425 | print "Screen closed" 426 | exit while 427 | else if (msg.isButtonPressed()) then 428 | print "Button pressed: "; msg.GetIndex(); " " msg.GetData() 429 | exit while 430 | else 431 | print "Unknown event: "; msg.GetType(); " msg: "; msg.GetMessage() 432 | exit while 433 | end if 434 | end if 435 | end while 436 | End Sub -------------------------------------------------------------------------------- /source/reddit.brs: -------------------------------------------------------------------------------- 1 | '****************************************************************************** 2 | ' reddit.brs 3 | ' Adds support for handling reddit's json feed for subreddits 4 | ' Documentation on the API is here: 5 | ' http://www.reddit.com/dev/api#section_listings 6 | '****************************************************************************** 7 | 8 | '****************************************************************************** 9 | ' Main function to begin displaying subreddit content 10 | ' @param youtube the current youtube instance 11 | ' @param url an optional URL with the multireddit to query, or the full link to parse. This is used when hitting the 'More Results' or 'Back' buttons on the video list page. 12 | ' multireddits look like this: videos+funny+humor for /r/videos, /r/funny, and /r/humor 13 | '****************************************************************************** 14 | Sub ViewReddits(youtube as Object, url = "videos" as String) 15 | screen = uitkPreShowPosterMenu("flat-episodic-16x9", "Reddit") 16 | screen.showMessage("Loading subreddits...") 17 | title = "Reddit" 18 | categories = RedditCategoryList() 19 | if (url = "videos") then 20 | tempSubs = RegRead("subreddits", "reddit") 21 | if (tempSubs <> invalid) then 22 | if (Len(tempSubs) > 0) then 23 | url = tempSubs 24 | end if 25 | end if 26 | end if 27 | categoryList = CreateObject("roArray", 100, true) 28 | for each category in categories 29 | categoryList.Push(category.title) 30 | next 31 | ' Category selection function 32 | oncontent_callback = [categories, m, 33 | function(categories, youtube, set_idx) 34 | if (categories.Count() > 0) then 35 | categories[set_idx].links.Clear() 36 | categories[set_idx].links.Push( categories[set_idx].link ) 37 | metadata = doQuery(categories[set_idx].link, false, categories[set_idx]) 38 | return metadata 39 | else 40 | return [] 41 | end if 42 | end function] 43 | ' Function that runs when a video/action arrow is selected 44 | onclick_callback = [categories, youtube, 45 | function(categories, youtube, video, category_idx, set_idx) 46 | if (video[set_idx]["action"] <> invalid) then 47 | linksList = categories[category_idx].links 48 | 49 | if (video[set_idx]["action"] = "next") then 50 | ' Last item is the next item link 51 | theLink = linksList.Peek() 52 | else 53 | ' Previous button - should only be visible if there are at least 3 items in the list 54 | ' The last item at this point is the 'next link' 55 | ' The second to last item is the current URL 56 | ' The third-to-last item is the previous URL 57 | 58 | ' This pops off the 'next link' which can be thrown away if we are going to the previous results 59 | linksList.Pop() 60 | if ( linksList.Count() > 1 ) then 61 | ' This pops off the 'current URL' which can be thrown away if we are going to the previous results, since it will 62 | ' be re-added via the doQuery call 63 | linksList.Pop() 64 | ' The final item is the previous item we meant to go view 65 | theLink = linksList.Peek() 66 | else 67 | ' If there is one item left in the list, leave it alone since it is the initial subreddit link 68 | theLink = linksList.Peek() 69 | end if 70 | end if 71 | if ( theLink = invalid ) then 72 | theLink = categories[category_idx].link 73 | end if 74 | ' Include a Back button, if there is more than one item left in the list 75 | previous = linksList.Count() > 1 76 | return { isContentList: true, content: doQuery( theLink, previous, categories[category_idx] ) } 77 | else 78 | youtube.VideoDetails(video[set_idx], "/r/" + categories[category_idx].title, video, set_idx) 79 | return { isContentList: false, content: video } 80 | end if 81 | end function] 82 | uitkDoCategoryMenu(categoryList, screen, oncontent_callback, onclick_callback, onplay_callback) 83 | End Sub 84 | 85 | '****************************************************************************** 86 | ' Helper function to query reddit, as well as build the metadata based on the response 87 | ' @param multireddits an optional URL with the multireddit to query, or the full link to parse. This is used when hitting the 'More Results' or 'Back' buttons on the video list page. 88 | ' multireddits look like this: videos+funny+humor for /r/videos, /r/funny, and /r/humor 89 | ' @param includePrevious should a previous button be included in the results metadata? 90 | ' @param categoryObject the (optional) category object for the current subreddit (category) 91 | '****************************************************************************** 92 | Function doQuery(multireddits = "videos" as String, includePrevious = false as Boolean, categoryObject = invalid as Dynamic) as Object 93 | response = QueryReddit(multireddits) 94 | if (response.status = 403) then 95 | ShowErrorDialog(title + " may be private, or unavailable at this time. Try again.", "403 Forbidden") 96 | return [] 97 | end if 98 | if (response.status <> 200 OR response.json = invalid OR response.json.kind <> "Listing") then 99 | ShowConnectionFailed() 100 | return [] 101 | end if 102 | 103 | ' Everything is OK, display the list 104 | json = response.json 105 | metadata = GetRedditMetaData(NewRedditVideoList(json.data.children)) 106 | 107 | ' Now add the 'More results' button 108 | for each link in response.links 109 | if (type(link) = "roAssociativeArray") then 110 | if (link.type = "next") then 111 | metadata.Push({shortDescriptionLine1: "More Results", action: "next", HDPosterUrl:"pkg:/images/icon_next_episode.jpg", SDPosterUrl:"pkg:/images/icon_next_episode.jpg"}) 112 | if ( categoryObject <> invalid ) then 113 | categoryObject.links.Push( link.href ) 114 | end if 115 | end if 116 | end if 117 | end for 118 | if ( includePrevious = true ) then 119 | metadata.Unshift({shortDescriptionLine1: "Back", action: "prev", HDPosterUrl:"pkg:/images/icon_prev_episode.jpg", SDPosterUrl:"pkg:/images/icon_prev_episode.jpg"}) 120 | end if 121 | 122 | return metadata 123 | End Function 124 | 125 | '****************************************************************************** 126 | ' Runs the query against the reddit servers, and handles parsing the response 127 | ' @param multireddits an optional URL with the multireddit to query, or the full link to parse. This is used when hitting the 'More Results' or 'Back' buttons on the video list page. 128 | ' multireddits look like this: videos+funny+humor for /r/videos, /r/funny, and /r/humor 129 | ' @return an roAssociativeArray containing the following members: 130 | ' json = the JSON object represented as an roAssociativeArray 131 | ' links = roArray of link objects containing the following members: 132 | ' func = callback function (ViewReddits) 133 | ' type = "next" or "previous" 134 | ' href = URL to the next or previous page of results 135 | ' status = the HTTP status code response from the GET call 136 | '****************************************************************************** 137 | Function QueryReddit(multireddits = "videos" as String) As Object 138 | method = "GET" 139 | if (Instr(0, multireddits, "http://")) then 140 | http = NewHttp(multireddits) 141 | else 142 | http = NewHttp("http://www.reddit.com/r/" + multireddits + "/hot.json") 143 | end if 144 | headers = { } 145 | 146 | http.method = method 147 | rsp = http.getToStringWithTimeout(10, headers) 148 | 149 | ' print "----------------------------------" 150 | ' print rsp 151 | ' print "----------------------------------" 152 | 153 | json = ParseJson(rsp) 154 | links = CreateObject("roArray", 1, true) 155 | if (json <> invalid) then 156 | if (json.data.after <> invalid) then 157 | link = CreateObject("roAssociativeArray") 158 | link.func = doQuery 159 | link.type = "next" 160 | http.RemoveParam("after", "urlParams") 161 | http.AddParam("after", json.data.after, "urlParams") 162 | link.href = http.GetURL() 163 | links.Push(link) 164 | end if 165 | ' Reddit doesn't give a "real" previous URL 166 | end if 167 | returnObj = CreateObject("roAssociativeArray") 168 | returnObj.json = json 169 | returnObj.links = links 170 | returnObj.status = http.status 171 | return returnObj 172 | End Function 173 | 174 | '****************************************************************************** 175 | ' Creates an roList of video objects, determining if they are from YouTube AND the ID was properly parsed from the URL 176 | ' @param jsonObject the JSON object that was received in QueryReddit 177 | ' @return an roList of video objects that are from YouTube AND have a valid video ID associated 178 | '****************************************************************************** 179 | Function NewRedditVideoList(jsonObject As Object) As Object 180 | videoList = CreateObject("roList") 181 | for each record in jsonObject 182 | domain = LCase(record.data.domain).Trim() 183 | if (domain = "youtube.com" OR domain = "youtu.be") then 184 | video = NewRedditVideo(record) 185 | if (video.GetID() <> invalid AND video.GetID() <> "") then 186 | videoList.Push(video) 187 | end if 188 | end if 189 | next 190 | return videoList 191 | End Function 192 | 193 | '******************************************************************** 194 | ' Creates the list of categories from the provided XML 195 | ' @return an roList, which will be sorted by the yt:unreadCount if the XML 196 | ' represents a list of subscriptions. 197 | '******************************************************************** 198 | Function RedditCategoryList() As Object 199 | categoryList = CreateObject("roList") 200 | subreddits = RegRead("subreddits", "reddit") 201 | if (RegRead("enabled", "reddit") = invalid) then 202 | if (subreddits <> invalid) then 203 | regex = CreateObject("roRegex", "\+", "") ' split on plus 204 | subredditArray = regex.Split(subreddits) 205 | else 206 | subredditArray = ["videos"] 207 | end if 208 | else 209 | subredditArray = [] 210 | end if 211 | for each record in subredditArray 212 | category = CreateObject("roAssociativeArray") 213 | category.title = record 214 | category.link = "http://www.reddit.com/r/" + record + "/hot.json" 215 | category.links = CreateObject("roList") 216 | category.links.Push(category.link) 217 | categoryList.Push(category) 218 | next 219 | return categoryList 220 | End Function 221 | 222 | '****************************************************************************** 223 | ' Creates a video roAssociativeArray, with the appropriate members needed to set Content Metadata and play a video with 224 | ' @param jsonObject the JSON "data" object that was received in QueryReddit, this is one result of many 225 | ' @return an roAssociativeArray of metadata for the current result 226 | ' TODO: There's no reason these are functions 227 | '****************************************************************************** 228 | Function NewRedditVideo(jsonObject As Object) As Object 229 | video = CreateObject("roAssociativeArray") 230 | video.json = jsonObject 231 | video.GetID = function() 232 | ' The URL needs to be decoded prior to attempting to match 233 | idMatches = LoadYouTube().ytIDRegex.Match( URLDecode( m.json.data.url) ) 234 | id = invalid 235 | if (idMatches.Count() > 1) then 236 | id = idMatches[1] 237 | end if 238 | return id 239 | end function 240 | video.GetTitle = function() 241 | return htmlDecode( m.json.data.title ) 242 | end function 243 | video.GetCategory = function(): return "/r/" + m.json.data.subreddit: end function 244 | video.GetDesc = function() 245 | desc = "" 246 | if ( m.json.data.media <> invalid AND m.json.data.media.oembed <> invalid ) then 247 | desc = m.json.data.media.oembed.description 248 | end if 249 | if ( desc = invalid ) then 250 | desc = "" 251 | end if 252 | return htmlDecode( desc ) 253 | end function 254 | video.GetScore = function(): return m.json.data.score : end function 255 | video.GetThumb = function() 256 | thumb = "" 257 | if (m.json.data.media <> invalid AND m.json.data.media.oembed <> invalid) then 258 | thumb = m.json.data.media.oembed.thumbnail_url 259 | end if 260 | return thumb 261 | end function 262 | video.GetURL = function() 263 | url = m.json.data.url 264 | if (m.json.data.media <> invalid AND m.json.data.media.oembed <> invalid) then 265 | url = m.json.data.media.oembed.url 266 | end if 267 | return url 268 | end function 269 | return video 270 | End Function 271 | 272 | '****************************************************************************** 273 | ' Custom metadata function needed to simplify displaying of content metadata for reddit results. 274 | ' This is necessary since the amount of metadata available for videos is much less than that available 275 | ' when querying YouTube directly. 276 | ' This will be called from doQuery 277 | ' It would be possible to Query YouTube for the additional metadata, but I don't know if that's worth it. 278 | ' @param videoList a list of video objects retrieved via the function NewRedditVideo 279 | ' @return an array of content metadata suitable for the Roku's screen objects. 280 | '****************************************************************************** 281 | Function GetRedditMetaData(videoList As Object) as Object 282 | metadata = [] 283 | 284 | for each video in videoList 285 | meta = CreateObject("roAssociativeArray") 286 | meta["ContentType"] = "movie" 287 | meta["ID"] = video.GetID() 288 | meta["TitleSeason"] = video.GetTitle() 289 | meta["Title"] = "Score: " + tostr(video.GetScore()) 290 | meta["Actors"] = meta.Title 291 | meta["Description"] = video.GetDesc() 292 | meta["Categories"] = video.GetCategory() 293 | meta["ShortDescriptionLine1"] = meta.TitleSeason 294 | meta["ShortDescriptionLine2"] = meta.Title 295 | meta["SDPosterUrl"] = video.GetThumb() 296 | meta["HDPosterUrl"] = video.GetThumb() 297 | meta["StreamFormat"] = "mp4" 298 | meta["Streams"] = [] 299 | metadata.Push(meta) 300 | end for 301 | 302 | return metadata 303 | End Function 304 | 305 | Sub EditRedditSettings() 306 | port = CreateObject("roMessagePort") 307 | screen = CreateObject("roSearchScreen") 308 | screen.SetMessagePort(port) 309 | 310 | history = CreateObject("roSearchHistory") 311 | subreddits = RegRead("subreddits", "reddit") 312 | if (RegRead("enabled", "reddit") = invalid) then 313 | if (subreddits <> invalid) then 314 | regex = CreateObject("roRegex", "\+", "") ' split on plus 315 | subredditArray = regex.Split(subreddits) 316 | else 317 | subredditArray = ["videos"] 318 | end if 319 | else 320 | subredditArray = [] 321 | end if 322 | screen.SetSearchTerms(subredditArray) 323 | screen.SetBreadcrumbText("", "Hit the * button to remove a subreddit") 324 | screen.SetSearchTermHeaderText("Current Subreddits:") 325 | screen.SetClearButtonText("Remove All") 326 | screen.SetSearchButtonText("Add Subreddit") 327 | screen.SetEmptySearchTermsText("The reddit channel will be disabled") 328 | screen.Show() 329 | 330 | while (true) 331 | msg = wait(0, port) 332 | 333 | if (type(msg) = "roSearchScreenEvent") then 334 | 'print "Event: "; msg.GetType(); " msg: "; msg.GetMessage() 335 | if (msg.isScreenClosed()) then 336 | exit while 337 | else if (msg.isPartialResult()) then 338 | ' Ignore it 339 | else if (msg.isFullResult()) then 340 | ' Check to see if they're trying to add a duplicate subreddit, or empty string 341 | newOne = msg.GetMessage() 342 | if (Len(newOne.Trim()) > 0) then 343 | found = false 344 | for each subreddit in subredditArray 345 | if (LCase(subreddit).Trim() = LCase(newOne).Trim()) then 346 | found = true 347 | exit for 348 | end if 349 | next 350 | if (not(found)) then 351 | if (subredditArray.Count() = 0) then 352 | subredditArray = [] 353 | end if 354 | subredditArray.Push(newOne) 355 | 356 | screen.SetSearchTerms(subredditArray) 357 | RegDelete("enabled", "reddit") 358 | end if 359 | end if 360 | else if (msg.isCleared()) then 361 | subredditArray.Clear() 362 | screen.ClearSearchTerms() 363 | RegWrite("enabled", "false", "reddit") 364 | else if ((msg.isRemoteKeyPressed() AND msg.GetIndex() = 10) OR msg.isButtonInfo()) then 365 | if (subredditArray.Count() > 0) then 366 | subredditArray.Delete(msg.GetIndex()) 367 | screen.SetSearchTerms(subredditArray) 368 | end if 369 | 'else 370 | 'print("Unhandled event on search screen") 371 | end if 372 | 'else 373 | 'print("Unhandled msg type: " + type(msg)) 374 | end if 375 | end while 376 | ' Save the user's subreddits when the settings screen is closing 377 | subString = "" 378 | if ( subredditArray.Count() > 0 ) then 379 | for i = 0 to subredditArray.Count() - 1 380 | subString = subString + subredditArray[i] 381 | if ( i < subredditArray.Count() - 1 ) then 382 | subString = subString + "+" 383 | end if 384 | next 385 | RegWrite("subreddits", subString, "reddit") 386 | else 387 | ' If their list is empty, just remove the unused registry key 388 | RegDelete("subreddits", "reddit") 389 | end if 390 | End Sub -------------------------------------------------------------------------------- /source/url.brs: -------------------------------------------------------------------------------- 1 | 2 | ' ****************************************************** 3 | ' 4 | ' Url Query builder 5 | ' 6 | ' To aid in percent-encoding url query parameters. 7 | ' In theory you can blindly encode the whole query (including ='s, &'s, etc) 8 | ' 9 | ' so this is a quick and dirty name/value encoder/accumulator 10 | ' 11 | ' The oauth protocol needs to interact with parameters in a 12 | ' particular way, so access to groups of parameters and 13 | ' their encodings are provided as well. 14 | ' 15 | ' Several callbacks can be placed on the returned http object 16 | ' by the calling code to be called by this code when appropriate: 17 | ' callbackPrep - called right before sending 18 | ' callbackRetry - called after failure if retries>0 19 | ' callbackCancel - called after failure if retries=0 20 | ' These allow side effects without explicitly coding them here. 21 | ' ****************************************************** 22 | 23 | Function NewHttp(url As String, port=invalid As Dynamic, method="GET" As String) as Object 24 | this = CreateObject("roAssociativeArray") 25 | this.port = port 26 | this.method = method 27 | this.anchor = "" 28 | this.label = "init" 29 | this.timeout = 5000 ' 5 secs 30 | this.retries = 1 31 | this.timer = CreateObject("roTimespan") 32 | this.timestamp = CreateObject("roTimespan") 33 | 34 | 'computed accessors 35 | this.Parse = http_parse 36 | this.AddParam = http_add_param 37 | this.AddAllParams = http_add_all_param 38 | this.RemoveParam = http_remove_param 39 | this.GetURL = http_get_url 40 | this.GetParams = http_get_params 41 | this.ParamGroup = http_get_param_group 42 | 43 | 'transfers 44 | this.GetToStringWithRetry = http_get_to_string_with_retry 45 | this.GetToStringWithTimeout = http_get_to_string_with_timeout 46 | this.PostFromStringWithTimeout = http_post_from_string_with_timeout 47 | 48 | this.Go = http_go 49 | this.Ok = http_ok 50 | this.Sync = http_sync 51 | this.Receive = http_receive 52 | this.Cancel = http_cancel 53 | this.CheckTimeout = http_check_timeout 54 | this.Retry = http_retry 55 | 56 | 'internal 57 | this.Prep = http_prep 58 | this.Wait = http_wait_with_timeout 59 | this.Dump = http_dump 60 | 61 | this.Parse(url) 62 | 63 | return this 64 | End Function 65 | 66 | ' ****************************************************** 67 | ' 68 | ' Setup the underlying http transfer object. 69 | ' 70 | ' ****************************************************** 71 | 72 | Function http_prep(method="" As String) 73 | ' this callback allows just-before-send 74 | ' mods to the request, e.g. timestamp 75 | if (isfunc(m.callbackPrep)) then 76 | m.callbackPrep() 77 | end if 78 | m.status = 0 79 | m.response = "" 80 | urlobj = CreateObject("roUrlTransfer") 81 | if (type(m.port) <> "roMessagePort") then 82 | m.port = CreateObject("roMessagePort") 83 | end if 84 | urlobj.SetPort(m.port) 85 | urlobj.SetCertificatesFile("common:/certs/ca-bundle.crt") 86 | urlobj.EnableEncodings(true) 87 | urlobj.AddHeader("Expect","") 88 | 'urlobj.RetainBodyOnError(true) 89 | url = m.GetUrl() 90 | urlobj.SetUrl(url) 91 | if (m.method <> "" AND m.method <> method) then 92 | m.method = method 93 | end if 94 | urlobj.SetRequest(m.method) 95 | HttpActive().replace(m,urlobj) 96 | m.timer.mark() 97 | End Function 98 | 99 | ' ****************************************************** 100 | ' 101 | ' Parse an url string into components of this object 102 | ' 103 | ' ****************************************************** 104 | 105 | Function http_parse(url As String) as Void 106 | remnant = CreateObject("roString") 107 | remnant.SetString(url) 108 | 109 | anchorBegin = Instr(1, remnant, "#") 110 | if (anchorBegin > 0) then 111 | if (anchorBegin < Len(remnant)) then 112 | m.anchor = Mid(remnant,anchorBegin + 1) 113 | end if 114 | remnant = Left(remnant,anchorbegin - 1) 115 | end if 116 | 117 | paramBegin = Instr(1, remnant, "?") 118 | if (paramBegin > 0) then 119 | if (paramBegin < Len(remnant)) then 120 | m.GetParams("urlParams").parse(Mid(remnant,paramBegin+1)) 121 | end if 122 | remnant = Left(remnant,paramBegin - 1) 123 | end if 124 | 125 | m.base = remnant 126 | End Function 127 | 128 | ' ****************************************************** 129 | ' 130 | ' Add an URL parameter to this object 131 | ' 132 | ' ****************************************************** 133 | 134 | Function http_add_param(name As String, val As String, group="" As String) 135 | params = m.GetParams(group) 136 | params.add(name,val) 137 | End Function 138 | 139 | 140 | Function http_add_all_param(keys as object, vals as object, group="" As String) 141 | params = m.GetParams(group) 142 | params.addall(keys,vals) 143 | End Function 144 | 145 | ' ****************************************************** 146 | ' 147 | ' remove an URL parameter from this object 148 | ' 149 | ' ****************************************************** 150 | 151 | Function http_remove_param(name As String, group="" As String) 152 | params = m.GetParams(group) 153 | params.remove(name) 154 | End Function 155 | 156 | ' ****************************************************** 157 | ' 158 | ' Get a named parameter list from this object 159 | ' 160 | ' ****************************************************** 161 | 162 | Function http_get_params(group="" As String) 163 | name = m.ParamGroup(group) 164 | if (not(m.DoesExist(name))) then 165 | m[name] = NewUrlParams() 166 | end if 167 | return m[name] 168 | End Function 169 | 170 | ' ****************************************************** 171 | ' 172 | ' Return the full encoded URL. 173 | ' 174 | ' ****************************************************** 175 | 176 | Function http_get_url() As String 177 | url = m.base 178 | params = m.GetParams("urlParams") 179 | if (not(params.empty())) then 180 | url = url + "?" 181 | end if 182 | url = url + params.encode() 183 | if (m.anchor <> "") then 184 | url = url + "#" + m.anchor 185 | end if 186 | return url 187 | End Function 188 | 189 | ' ****************************************************** 190 | ' 191 | ' Return the parameter group name, 192 | ' correctly defaulted if necessary. 193 | ' 194 | ' ****************************************************** 195 | 196 | Function http_get_param_group(group="" as String) 197 | if (group = "") then 198 | if (m.method = "POST") then 199 | name = "bodyParams" 200 | else 201 | name = "urlParams" 202 | end if 203 | else 204 | name = group 205 | end if 206 | return name 207 | End Function 208 | 209 | ' ****************************************************** 210 | ' 211 | ' Performs Http.AsyncGetToString() in a retry loop 212 | ' with exponential backoff. To the outside 213 | ' world this appears as a synchronous API. 214 | ' 215 | ' Return empty string on timeout 216 | ' 217 | ' ****************************************************** 218 | 219 | Function http_get_to_string_with_retry() as String 220 | 221 | timeout% = 2 222 | num_retries% = 5 223 | 224 | while (num_retries% > 0) 225 | ' print "Http: get tries left " + itostr(num_retries%) 226 | m.Prep("GET") 227 | if (m.Http.AsyncGetToString()) then 228 | if (m.Wait(timeout%)) then 229 | exit while 230 | end if 231 | timeout% = 2 * timeout% 232 | end if 233 | num_retries% = num_retries% - 1 234 | end while 235 | 236 | return m.response 237 | End Function 238 | 239 | ' ****************************************************** 240 | ' 241 | ' Performs Http.AsyncGetToString() with a single timeout in seconds 242 | ' To the outside world this appears as a synchronous API. 243 | ' 244 | ' Return empty string on timeout 245 | ' 246 | ' ****************************************************** 247 | 248 | Function http_get_to_string_with_timeout(seconds as Integer, headers=invalid As Object) as String 249 | if (m.method = invalid) then 250 | m.method = "GET" 251 | end if 252 | m.Prep(m.method) 253 | 254 | if (headers <> invalid) then 255 | for each key in headers 256 | print key,headers[key] 257 | m.Http.AddHeader(key, headers[key]) 258 | end for 259 | end if 260 | 261 | if (m.Http.AsyncGetToString()) then 262 | m.Wait(seconds) 263 | end if 264 | return m.response 265 | End Function 266 | 267 | ' ****************************************************** 268 | ' 269 | ' Performs Http.AsyncPostFromString() with a single timeout in seconds 270 | ' To the outside world this appears as a synchronous API. 271 | ' 272 | ' Return empty string on timeout 273 | ' 274 | ' ****************************************************** 275 | 276 | Function http_post_from_string_with_timeout(val As String, seconds as Integer, headers=invalid As Object) as String 277 | if (m.method = invalid) then 278 | m.method = "POST" 279 | end if 280 | m.Prep(m.method) 281 | 282 | if (headers <> invalid) then 283 | for each key in headers 284 | print key,headers[key] 285 | m.Http.AddHeader(key, headers[key]) 286 | end for 287 | end if 288 | 289 | if (m.Http.AsyncPostFromString(val)) then 290 | m.Wait(seconds) 291 | end if 292 | return m.response 293 | End Function 294 | 295 | ' ****************************************************** 296 | ' 297 | ' Common wait() for all the synchronous http transfers 298 | ' 299 | ' ****************************************************** 300 | 301 | Function http_wait_with_timeout(seconds As Integer) As Boolean 302 | id = HttpActive().ID(m) 303 | while (m.status = 0) 304 | nextTimeout = 1000 * seconds - m.timer.TotalMilliseconds() 305 | if (seconds > 0 AND nextTimeout <= 0) then 306 | exit while 307 | end if 308 | event = wait(nextTimeout, m.Http.GetPort()) 309 | if (type(event) = "roUrlEvent") then 310 | HttpActive().receive(event) 311 | else if (event = invalid) then 312 | m.cancel() 313 | else 314 | print "Http: unhandled event "; type(event) 315 | end if 316 | end while 317 | HttpActive().removeID(id) 318 | m.Dump() 319 | return m.Ok() 320 | End Function 321 | 322 | Function http_receive(msg As Object) 323 | m.status = msg.GetResponseCode() 324 | m.response = msg.GetString() 325 | m.label = "done" 326 | End Function 327 | 328 | Function http_cancel() 329 | m.Http.AsyncCancel() 330 | m.status = -1 331 | m.label = "cancel" 332 | m.dump() 333 | HttpActive().remove(m) 334 | End Function 335 | 336 | Function http_go(method="" As String) As Boolean 337 | ok = false 338 | m.Prep(method) 339 | if (m.method = "POST" OR m.method = "PUT") then 340 | ok = m.http.AsyncPostFromString(m.getParams().encode()) 341 | else if (m.method = "GET" OR m.method = "DELETE" OR m.method = "") 342 | ok = m.http.AsyncGetToString() 343 | else 344 | print "Http: "; m.method; " is not supported" 345 | end if 346 | m.label = "sent" 347 | 'm.Dump() 348 | return ok 349 | End Function 350 | 351 | Function http_ok() As Boolean 352 | ' depends on m.status which is updated by m.Wait() 353 | statusGroup = int(m.status/100) 354 | return statusGroup=2 or statusGroup=3 355 | End Function 356 | 357 | Function http_sync(seconds As Integer) As Boolean 358 | if (m.Go()) then 359 | m.Wait(seconds) 360 | end if 361 | return m.Ok() 362 | End Function 363 | 364 | Function http_dump() 365 | time = "unknown" 366 | if (m.DoesExist("timer")) then 367 | time = itostr(m.timer.TotalMilliseconds()) 368 | end if 369 | print "Http: #"; m.Http.GetIdentity(); " "; m.label; " status:"; m.status; " time: "; time; "ms request: "; m.method; " "; m.Http.GetURL() 370 | if (not(m.GetParams("bodyParams").empty())) then 371 | print " body: "; m.GetParams("bodyParams").encode() 372 | end if 373 | End Function 374 | 375 | Function http_check_timeout(defaultTimeout=0 As Integer) As Integer 376 | timeLeft = m.timeout-m.timer.TotalMilliseconds() 377 | if (timeLeft <= 0) then 378 | m.retry() 379 | timeLeft = defaultTimeout 380 | end if 381 | return timeLeft 382 | End Function 383 | 384 | Function http_retry(defaultTimeout=0 As Integer) As Integer 385 | m.cancel() 386 | if (m.retries > 0) then 387 | m.retries = m.retries - 1 388 | if (isfunc(m.callbackRetry)) then 389 | m.callbackRetry() 390 | else 391 | m.go() 392 | end if 393 | else if (isfunc(m.callbackCancel)) then 394 | m.callbackCancel() 395 | end if 396 | End Function 397 | 398 | ' ****************************************************** 399 | ' 400 | ' Operations on a collection of URL parameters 401 | ' 402 | ' ****************************************************** 403 | 404 | Function NewUrlParams(encoded="" As String, separator="&" As String) As Object 405 | 'stores the unencoded parameters in sorted order 406 | this = CreateObject("roAssociativeArray") 407 | this.names = CreateObject("roArray",0,true) 408 | this.params = CreateObject("roAssociativeArray") 409 | this.params.SetModeCaseSensitive() 410 | 411 | this.encode = params_encode 412 | this.parse = params_parse 413 | this.add = params_add 414 | this.addReplace = params_add_replace 415 | this.addAll = params_add_all 416 | this.remove = params_remove 417 | this.empty = params_empty 418 | this.get = params_get 419 | this.separator = separator 420 | this.parse(encoded) 421 | return this 422 | End Function 423 | 424 | Function params_encode() As String 425 | encodedParams = "" 426 | m.names.reset() 427 | while (m.names.isNext()) 428 | name = m.names.Next() 429 | encodedParams = encodedParams + URLEncode(name) + "=" + URLEncode(m.params[name]) 430 | if (m.names.isNext()) then 431 | encodedParams = encodedParams + m.separator 432 | end if 433 | end while 434 | return encodedParams 435 | End Function 436 | 437 | Function params_parse(encoded_params As String) as Object 438 | params = strTokenize(encoded_params,m.separator) 439 | for each paramExpr in params 440 | param = strTokenize(paramExpr,"=") 441 | if (param.Count() = 2) then 442 | m.addReplace(UrlDecode(param[0]),UrlDecode(param[1])) 443 | end if 444 | end for 445 | return m 446 | End Function 447 | 448 | Function params_add(name As String, val As String) as Object 449 | if (not(m.params.DoesExist(name))) then 450 | SortedInsert(m.names, name) 451 | m.params[name] = val 452 | end if 453 | return m 454 | End Function 455 | 456 | Function params_add_replace(name As String, val As String) as Object 457 | if (m.params.DoesExist(name)) then 458 | m.params[name] = val 459 | else 460 | m.add(name,val) 461 | end if 462 | return m 463 | End Function 464 | 465 | Function params_add_all(keys as Object, vals as object) as Object 466 | ' keys is an array 467 | ' vals is an array 468 | i = 0 469 | for each name in keys 470 | if (not(m.params.DoesExist(name))) then 471 | m.names.push(name) 472 | end if 473 | m.params[name] = vals[i] 474 | i = i + 1 475 | end for 476 | QuickSort(m.names) 477 | return m 478 | End Function 479 | 480 | sub params_remove(name As String) 481 | if (m.params.delete(name)) then 482 | n = 0 483 | while (n < m.names.count()) 484 | if (name = m.names[n]) then 485 | m.names.delete(n) 486 | return 487 | end if 488 | n = n + 1 489 | end while 490 | end if 491 | End sub 492 | 493 | Function params_empty() as Boolean 494 | return (m.params.IsEmpty()) 495 | End Function 496 | 497 | Function params_get(name As String) as String 498 | return validstr(m.params[name]) 499 | End Function 500 | 501 | 502 | ' ****************************************************** 503 | ' 504 | ' URLEncode - strict URL encoding of a string 505 | ' 506 | ' ****************************************************** 507 | 508 | Function URLEncode(str As String) As String 509 | if (not(m.DoesExist("encodeProxyUrl"))) then 510 | m.encodeProxyUrl = CreateObject("roUrlTransfer") 511 | end if 512 | return m.encodeProxyUrl.urlEncode(str) 513 | End Function 514 | 515 | ' ****************************************************** 516 | ' 517 | ' URLDecode - strict URL decoding of a string 518 | ' 519 | ' ****************************************************** 520 | 521 | Function URLDecode(str As String) As String 522 | strReplace(str,"+"," ") ' backward compatibility 523 | if (not(m.DoesExist("encodeProxyUrl"))) then 524 | m.encodeProxyUrl = CreateObject("roUrlTransfer") 525 | end if 526 | return m.encodeProxyUrl.Unescape(str) 527 | End Function 528 | 529 | ' 530 | ' map of identity to active http objects 531 | ' 532 | Function HttpActive() As Object 533 | ' singleton factory 534 | ha = m.HttpActive 535 | if (ha = invalid) then 536 | ha = CreateObject("roAssociativeArray") 537 | ha.actives = CreateObject("roAssociativeArray") 538 | ha.icount = 0 539 | ha.defaultTimeout = 30000 ' 30 secs 540 | ha.checkTimeouts = http_active_checkTimeouts 541 | ha.count = http_active_count 542 | ha.receive = http_active_receive 543 | ' by http obj 544 | ha.id = http_active_id 545 | ha.add = http_active_add 546 | ha.remove = http_active_remove 547 | ha.replace = http_active_replace 548 | ' by ID 549 | ha.getID = http_active_getID 550 | ha.removeID = http_active_removeID 551 | ha.total = strtoi(validstr(RegRead("Http.total","Debug"))) 552 | m.HttpActive = ha 553 | end if 554 | return ha 555 | End Function 556 | 557 | Function http_active_count() As Dynamic 558 | return m.icount 559 | End Function 560 | 561 | Function http_active_receive(msg As Object) As Dynamic 562 | id = msg.GetSourceIdentity() 563 | http = m.getID(id) 564 | if (http <> invalid) then 565 | http.receive(msg) 566 | else 567 | print "Http: #"; id; " discarding unidentifiable http response" 568 | print "Http: #"; id; " status"; msg.GetResponseCode() 569 | print "Http: #"; id; " response"; chr(10); msg.GetString() 570 | end if 571 | return http 572 | end Function 573 | 574 | Function http_active_id(http As Object) As Dynamic 575 | id = invalid 576 | if (http.DoesExist("http")) then 577 | id = http.http.GetIdentity() 578 | end if 579 | 'print "Http: got identity #"; id 580 | return id 581 | End Function 582 | 583 | Function http_active_add(http As Object) 584 | id = m.ID(http) 585 | if (id <> invalid) then 586 | 'print "Http: #"; id; " adding to active" 587 | m.actives[itostr(id)] = http 588 | m.icount = m.icount + 1 589 | m.total = m.total + 1 590 | if (wrap(m.total,50) = 0) then 591 | RegWrite("Http.total",itostr(m.total),"Debug") 592 | print "Http: total requests"; m.total 593 | end if 594 | end if 595 | End Function 596 | 597 | Function http_active_remove(http As Object) 598 | id = m.ID(http) 599 | if (id <> invalid) then 600 | m.removeID(id) 601 | end if 602 | End Function 603 | 604 | Function http_active_replace(http As Object, urlXfer As Object) 605 | m.remove(http) 606 | http.http = urlXfer 607 | m.add(http) 608 | End Function 609 | 610 | Function http_active_getID(id As Integer) As Dynamic 611 | return m.actives[itostr(id)] 612 | End Function 613 | 614 | Function http_active_removeID(id As Integer) 615 | strID = itostr(id) 616 | if (m.actives.DoesExist(strID)) then 617 | 'print "Http: #"; id; " removing from active" 618 | m.actives.delete(strID) 619 | m.icount = m.icount -1 620 | end if 621 | End Function 622 | 623 | Function http_active_checkTimeouts() As Integer 624 | defaultTimeout = m.defaultTimeout 625 | timeLeft = defaultTimeout 626 | for each id in m.actives 627 | active = m.actives[id] 628 | activeTL = active.checkTimeout(defaultTimeout) 629 | if (activeTL pivot) 68 | j = j - 1 69 | end while 70 | if (i <= j) then 71 | tmp = A[i] 72 | A[i] = A[j] 73 | A[j] = tmp 74 | i = i + 1 75 | j = j - 1 76 | end if 77 | end while 78 | if (left < j) then 79 | internalQSort(A, left, j) 80 | end if 81 | if (i < right) then 82 | internalQSort(A, i, right) 83 | end if 84 | End Function 85 | 86 | ' quicksort an array using a function to extract the compare value 87 | Function internalKeyQSort(A as Object, key as object, left as integer, right as integer) as void 88 | i = left 89 | j = right 90 | pivot = key(A[(left+right)/2]) 91 | while (i <= j) 92 | while (key(A[i]) < pivot) 93 | i = i + 1 94 | end while 95 | while (key(A[j]) > pivot) 96 | j = j - 1 97 | end while 98 | if (i <= j) then 99 | tmp = A[i] 100 | A[i] = A[j] 101 | A[j] = tmp 102 | i = i + 1 103 | j = j - 1 104 | end if 105 | end while 106 | if (left < j) then 107 | internalKeyQSort(A, key, left, j) 108 | end if 109 | if (i < right) then 110 | internalKeyQSort(A, key, i, right) 111 | end if 112 | End Function 113 | 114 | ' quicksort an array using an indentically sized array that holds the comparison values 115 | Function internalKeyArrayQSort(A as Object, keys as object, left as integer, right as integer) as void 116 | i = left 117 | j = right 118 | pivot = keys[A[(left+right)/2]] 119 | while (i <= j) 120 | while (keys[A[i]] < pivot) 121 | i = i + 1 122 | end while 123 | while (keys[A[j]] > pivot) 124 | j = j - 1 125 | end while 126 | if (i <= j) then 127 | tmp = A[i] 128 | A[i] = A[j] 129 | A[j] = tmp 130 | i = i + 1 131 | j = j - 1 132 | end if 133 | end while 134 | if (left < j) then 135 | internalKeyArrayQSort(A, keys, left, j) 136 | end if 137 | if (i < right) then 138 | internalKeyArrayQSort(A, keys, i, right) 139 | end if 140 | End function 141 | 142 | '****************************************************** 143 | ' QuickSort(Array, optional keys function or array) 144 | ' Will sort an array directly 145 | ' If key is a function it is called to get the value for comparison 146 | ' If key is an identically sized array as the array to be sorted then 147 | ' the comparison values are pulled from there. In this case the Array 148 | ' to be sorted should be an array if integers 0 .. arraysize-1 149 | '****************************************************** 150 | Sub QuickSort(A as Object, key=invalid as dynamic) 151 | atype = type(A) 152 | if (atype <> "roArray") then 153 | return 154 | end if 155 | ' weed out trivial arrays 156 | arraysize = A.Count() 157 | if (arraysize < 2) then 158 | return 159 | end if 160 | if (key = invalid) then 161 | internalQSort(A, 0, arraysize - 1) 162 | else 163 | keytype = type(key) 164 | if (keytype = "Function") then 165 | internalKeyQSort(A, key, 0, arraysize - 1) 166 | else if ((keytype="roArray" or keytype="Array") and (key.count() = arraysize)) then 167 | internalKeyArrayQSort(A, key, 0, arraysize - 1) 168 | end if 169 | end if 170 | End Sub 171 | 172 | '****************************************************** 173 | 'Insertion Sort 174 | 'Will sort an array directly, or use a key function 175 | '****************************************************** 176 | Sub Sort(A as Object, key = invalid as Dynamic) 177 | 178 | if (type(A) <> "roArray" AND type(A) <> "roList") then 179 | return 180 | end if 181 | 182 | if (key = invalid) then 183 | for i = 1 to A.Count()-1 184 | value = A[i] 185 | j = i-1 186 | while (j >= 0 and A[j] < value) 187 | A[j + 1] = A[j] 188 | j = j-1 189 | end while 190 | A[j+1] = value 191 | next 192 | else 193 | if (type(key) <> "Function") then 194 | return 195 | end if 196 | for i = 1 to A.Count()-1 197 | valuekey = key(A[i]) 198 | value = A[i] 199 | j = i-1 200 | while (j >= 0 and key(A[j]) < valuekey) 201 | A[j + 1] = A[j] 202 | j = j-1 203 | end while 204 | A[j+1] = value 205 | next 206 | 207 | end if 208 | 209 | End Sub 210 | 211 | ' insert value into array 212 | Sub SortedInsert(A as object, value as string) 213 | count = a.count() 214 | a.push(value) ' use push to make sure array size is correct now 215 | if (count = 0) then 216 | return 217 | end if 218 | ' should do a binary search, but at least this is better than push and sort 219 | for i = count-1 to 0 step -1 220 | if (value >= a[i]) then 221 | a[i+1] = value 222 | return 223 | end if 224 | a[i+1] = a[i] 225 | end for 226 | a[0] = value 227 | End Sub 228 | 229 | 230 | '******************************************************' 231 | '* MISC UTILITIES *' 232 | '******************************************************' 233 | 234 | '****************************************************** 235 | 'Convert anything to a string 236 | ' 237 | 'Always returns a string 238 | '****************************************************** 239 | Function tostr(any) as String 240 | ret = AnyToString(any) 241 | if (ret = invalid) then 242 | ret = type(any) 243 | end if 244 | if (ret = invalid) then 245 | ret = "unknown" 'failsafe 246 | end if 247 | return ret 248 | End Function 249 | 250 | '****************************************************** 251 | 'Get a " char as a string 252 | '****************************************************** 253 | Function Quote() as String 254 | q$ = Chr(34) 255 | return q$ 256 | End Function 257 | 258 | '****************************************************** 259 | 'Determine if the given object supports the ifXMLElement interface 260 | '****************************************************** 261 | Function isxmlelement(obj as dynamic) As Boolean 262 | if (obj = invalid) then 263 | return false 264 | end if 265 | if (GetInterface(obj, "ifXMLElement") = invalid) then 266 | return false 267 | end if 268 | return true 269 | End Function 270 | 271 | 272 | '****************************************************** 273 | 'Determine if the given object supports the ifList interface 274 | '****************************************************** 275 | Function islist(obj as dynamic) As Boolean 276 | if (obj = invalid) then 277 | return false 278 | end if 279 | if (GetInterface(obj, "ifArray") = invalid) then 280 | return false 281 | end if 282 | return true 283 | End Function 284 | 285 | '****************************************************** 286 | ' Determine if the given object supports the ifInt interface 287 | '****************************************************** 288 | Function isint(obj as dynamic) As Boolean 289 | if (obj = invalid) then 290 | return false 291 | end if 292 | if (GetInterface(obj, "ifInt") = invalid) then 293 | return false 294 | end if 295 | return true 296 | End Function 297 | 298 | '****************************************************** 299 | ' Determine if the given argument is a function 300 | ' @param obj the object to test 301 | ' @return true if obj is a Function, false if it is not 302 | '****************************************************** 303 | Function isfunc(obj as dynamic) As Boolean 304 | tf = type(obj) 305 | return (tf = "Function" or tf = "roFunction") 306 | End Function 307 | 308 | '****************************************************** 309 | ' always return a valid string. if the argument is 310 | ' invalid or not a string, return an empty string 311 | '****************************************************** 312 | Function validstr(obj As Dynamic) As String 313 | if (isnonemptystr(obj)) then 314 | return obj 315 | end if 316 | return "" 317 | End Function 318 | 319 | '****************************************************** 320 | ' Determine if the given object supports the ifString interface 321 | '****************************************************** 322 | Function isstr(obj as dynamic) As Boolean 323 | if (obj = invalid) then 324 | return false 325 | end if 326 | if (GetInterface(obj, "ifString") = invalid) then 327 | return false 328 | end if 329 | return true 330 | End Function 331 | 332 | '****************************************************** 333 | ' Determine if the given object supports the ifString interface 334 | ' and returns a string of non zero length 335 | '****************************************************** 336 | Function isnonemptystr(obj) 337 | if (isnullorempty(obj)) then 338 | return false 339 | end if 340 | return true 341 | End Function 342 | 343 | '****************************************************** 344 | ' Determine if the given object is invalid or supports 345 | ' the ifString interface and returns a string of non zero length 346 | '****************************************************** 347 | Function isnullorempty(obj) as Boolean 348 | if (obj = invalid) then 349 | return true 350 | end if 351 | if (not isstr(obj)) then 352 | return true 353 | end if 354 | if (Len(obj) = 0) then 355 | return true 356 | end if 357 | return false 358 | End Function 359 | 360 | '****************************************************** 361 | ' Determine if the given object supports the ifBoolean interface 362 | '****************************************************** 363 | Function isbool(obj as dynamic) As Boolean 364 | if (obj = invalid) then 365 | return false 366 | end if 367 | if (GetInterface(obj, "ifBoolean") = invalid) then 368 | return false 369 | end if 370 | return true 371 | End Function 372 | 373 | 374 | '****************************************************** 375 | ' Determine if the given object supports the ifFloat interface 376 | '****************************************************** 377 | Function isfloat(obj as dynamic) As Boolean 378 | if (obj = invalid) then 379 | return false 380 | end if 381 | if (GetInterface(obj, "ifFloat") = invalid) then 382 | return false 383 | end if 384 | return true 385 | End Function 386 | 387 | 388 | '****************************************************** 389 | ' Convert string to boolean safely. Don't crash 390 | ' Looks for certain string values 391 | '****************************************************** 392 | Function strtobool(obj As dynamic) As Boolean 393 | if (obj = invalid) then 394 | return false 395 | end if 396 | if (type(obj) <> "roString") then 397 | return false 398 | end if 399 | o = strTrim(obj) 400 | o = Lcase(o) 401 | if (o = "true") then 402 | return true 403 | end if 404 | if (o = "t") then 405 | return true 406 | end if 407 | if (o = "y") then 408 | return true 409 | end if 410 | if (o = "1") then 411 | return true 412 | end if 413 | return false 414 | End Function 415 | 416 | '****************************************************** 417 | ' Convert int to string. This is necessary because 418 | ' the builtin Stri(x) prepends whitespace 419 | '****************************************************** 420 | Function itostr(i As Integer) As String 421 | str = Stri(i) 422 | return strTrim(str) 423 | End Function 424 | 425 | '****************************************************** 426 | ' Get 'aining hours from a total seconds 427 | '****************************************************** 428 | Function hoursLeft(seconds As Integer) As Integer 429 | hours% = seconds / 3600 430 | return hours% 431 | End Function 432 | 433 | '****************************************************** 434 | ' Get 'aining minutes from a total seconds 435 | '****************************************************** 436 | Function minutesLeft(seconds As Integer) As Integer 437 | hours% = seconds / 3600 438 | mins% = seconds - (hours% * 3600) 439 | mins% = mins% / 60 440 | return mins% 441 | End Function 442 | 443 | '****************************************************** 444 | ' Trim a string 445 | '****************************************************** 446 | Function strTrim(str As String) As String 447 | st = CreateObject("roString") 448 | st.SetString(str) 449 | return st.Trim() 450 | End Function 451 | 452 | 453 | '****************************************************** 454 | ' Tokenize a string. Return roList of strings 455 | '****************************************************** 456 | Function strTokenize(str As String, delim As String) As Object 457 | st = CreateObject("roString") 458 | st.SetString(str) 459 | return st.Tokenize(delim) 460 | End Function 461 | 462 | '****************************************************** 463 | ' Decodes HTML entities like & < etc. 464 | '****************************************************** 465 | Function htmlDecode( encodedStr as String ) as String 466 | result = strReplace( encodedStr, "&", "&" ) 467 | result = strReplace( result, "<", "<" ) 468 | result = strReplace( result, ">", ">" ) 469 | result = strReplace( result, " ", " " ) 470 | result = strReplace( result, "'", "'" ) 471 | result = strReplace( result, """, Quote() ) 472 | return result 473 | End Function 474 | 475 | '****************************************************** 476 | ' Replace substrings in a string. Return new string 477 | '****************************************************** 478 | Function strReplace(basestr As String, oldsub As String, newsub As String) As String 479 | newstr = "" 480 | 481 | i = 1 482 | while (i <= Len(basestr)) 483 | x = Instr(i, basestr, oldsub) 484 | if (x = 0) then 485 | newstr = newstr + Mid(basestr, i) 486 | exit while 487 | end if 488 | 489 | if (x > i) then 490 | newstr = newstr + Mid(basestr, i, x-i) 491 | i = x 492 | end if 493 | 494 | newstr = newstr + newsub 495 | i = i + Len(oldsub) 496 | end while 497 | 498 | return newstr 499 | End Function 500 | 501 | '****************************************************** 502 | ' Get all XML subelements by name 503 | ' 504 | ' return list of 0 or more elements 505 | '****************************************************** 506 | Function GetXMLElementsByName(xml As Object, name As String) As Object 507 | list = CreateObject("roArray", 100, true) 508 | if (not(islist(xml.GetBody()))) then 509 | return list 510 | end if 511 | 512 | for each e in xml.GetBody() 513 | if (e.GetName() = name) then 514 | list.Push(e) 515 | end if 516 | next 517 | 518 | return list 519 | End Function 520 | 521 | '****************************************************** 522 | ' Get all XML subelement's string bodies by name 523 | ' 524 | ' return list of 0 or more strings 525 | '****************************************************** 526 | Function GetXMLElementBodiesByName(xml As Object, name As String) As Object 527 | list = CreateObject("roArray", 100, true) 528 | if (not(islist(xml.GetBody()))) then 529 | return list 530 | end if 531 | 532 | for each e in xml.GetBody() 533 | if (e.GetName() = name) then 534 | b = e.GetBody() 535 | if (type(b) = "roString") then 536 | list.Push(b) 537 | end if 538 | end if 539 | next 540 | 541 | return list 542 | End Function 543 | 544 | '****************************************************** 545 | ' Get first XML subelement by name 546 | ' 547 | ' return invalid if not found, else the element 548 | '****************************************************** 549 | Function GetFirstXMLElementByName(xml As Object, name As String) As Dynamic 550 | if (not(islist(xml.GetBody()))) then 551 | return invalid 552 | end if 553 | 554 | for each e in xml.GetBody() 555 | if (e.GetName() = name) then 556 | return e 557 | end if 558 | next 559 | 560 | return invalid 561 | End Function 562 | 563 | '****************************************************** 564 | ' Get first XML subelement's string body by name 565 | ' 566 | ' return invalid if not found, else the subelement's body string 567 | '****************************************************** 568 | Function GetFirstXMLElementBodyStringByName(xml As Object, name As String) As Dynamic 569 | e = GetFirstXMLElementByName(xml, name) 570 | if (e = invalid) then 571 | return invalid 572 | end if 573 | if (type(e.GetBody()) <> "roString") then 574 | return invalid 575 | end if 576 | return e.GetBody() 577 | End Function 578 | 579 | '****************************************************** 580 | ' Get the xml element as an integer 581 | ' 582 | ' return invalid if body not a string, else the integer as converted by strtoi 583 | '****************************************************** 584 | Function GetXMLBodyAsInteger(xml As Object) As Dynamic 585 | if (type(xml.GetBody()) <> "roString") then 586 | return invalid 587 | end if 588 | return strtoi(xml.GetBody()) 589 | End Function 590 | 591 | 592 | '****************************************************** 593 | ' Parse a string into a roXMLElement 594 | ' 595 | ' return invalid on error, else the xml object 596 | '****************************************************** 597 | Function ParseXML(str As String) As Dynamic 598 | if (str = invalid) then 599 | return invalid 600 | end if 601 | xml = CreateObject("roXMLElement") 602 | if (not(xml.Parse(str))) then 603 | return invalid 604 | end if 605 | return xml 606 | End Function 607 | 608 | '****************************************************** 609 | ' Get XML sub elements whose bodies are strings into an associative array. 610 | ' subelements that are themselves parents are skipped 611 | ' namespace :'s are replaced with _'s 612 | ' 613 | ' So an XML element like... 614 | ' 615 | ' 616 | ' abcdefg 617 | ' xyz 618 | ' 619 | ' 620 | ' .... 621 | ' 622 | ' 623 | ' homer 624 | ' 625 | ' 626 | ' returns an AA with: 627 | ' 628 | ' aa.This = "abcdefg" 629 | ' aa.Sucks = "xyz" 630 | ' aa.ns_doh = "homer" 631 | ' 632 | ' return an empty AA if nothing found 633 | '****************************************************** 634 | Sub GetXMLintoAA(xml As Object, aa As Object) 635 | for each e in xml.GetBody() 636 | body = e.GetBody() 637 | if (type(body) = "roString") then 638 | name = e.GetName() 639 | name = strReplace(name, ":", "_") 640 | aa.AddReplace(name, body) 641 | end if 642 | next 643 | End Sub 644 | 645 | '****************************************************** 646 | ' Walk an AA and print it 647 | '****************************************************** 648 | Sub PrintAA(aa as Object) 649 | print "---- AA ----" 650 | if (aa = invalid) then 651 | print "invalid" 652 | return 653 | else 654 | cnt = 0 655 | for each e in aa 656 | x = aa[e] 657 | PrintAny(0, e + ": ", aa[e]) 658 | cnt = cnt + 1 659 | next 660 | if (cnt = 0) then 661 | PrintAny(0, "Nothing from foreach. Looks like :", aa) 662 | end if 663 | end if 664 | print "------------" 665 | End Sub 666 | 667 | '****************************************************** 668 | ' Walk a list and print it 669 | '****************************************************** 670 | Sub PrintList(list as Object, header = "" as String) 671 | if ( header <> "" ) then 672 | print( "---- " + header + " ----") 673 | else 674 | print( "---- list ----" ) 675 | end if 676 | PrintAnyList(0, list) 677 | print "--------------" 678 | End Sub 679 | 680 | '****************************************************** 681 | ' Print an associativearray 682 | '****************************************************** 683 | Sub PrintAnyAA(depth As Integer, aa as Object) 684 | for each e in aa 685 | x = aa[e] 686 | PrintAny(depth, e + ": ", aa[e]) 687 | next 688 | End Sub 689 | 690 | '****************************************************** 691 | ' Print a list with indent depth 692 | '****************************************************** 693 | Sub PrintAnyList(depth As Integer, list as Object) 694 | i = 0 695 | for each e in list 696 | PrintAny(depth, "List(" + itostr(i) + ")= ", e) 697 | i = i + 1 698 | next 699 | End Sub 700 | 701 | '****************************************************** 702 | ' Print anything 703 | '****************************************************** 704 | Sub PrintAny(depth As Integer, prefix As String, any As Dynamic) 705 | if (depth >= 10) then 706 | print "**** TOO DEEP, limiting to 10.. " + itostr(5) 707 | depth = 10 708 | end if 709 | prefix = string(depth * 2," ") + prefix 710 | depth = depth + 1 711 | str = AnyToString(any) 712 | if (str <> invalid) then 713 | print prefix + str 714 | return 715 | end if 716 | if (type(any) = "roAssociativeArray") then 717 | print prefix + "(assocarr)..." 718 | PrintAnyAA(depth, any) 719 | return 720 | end if 721 | if (islist(any) = true) then 722 | print prefix + "(list of " + itostr(any.Count()) + ")..." 723 | PrintAnyList(depth, any) 724 | return 725 | end if 726 | 727 | print prefix + "?" + type(any) + "?" 728 | End Sub 729 | 730 | '****************************************************** 731 | ' Print an object as a string for debugging. If it is 732 | ' very long print the first 500 chars. 733 | '****************************************************** 734 | Sub Dbg(pre As Dynamic, o=invalid As Dynamic) 735 | p = AnyToString(pre) 736 | if (p = invalid) then 737 | p = "" 738 | end if 739 | if (o = invalid) then 740 | o = "" 741 | end if 742 | s = AnyToString(o) 743 | if (s = invalid) then 744 | s = "???: " + type(o) 745 | end if 746 | if (Len(s) > 4000) then 747 | s = Left(s, 4000) 748 | end if 749 | print p + s 750 | End Sub 751 | 752 | '****************************************************** 753 | ' Try to convert anything to a string. Only works on simple items. 754 | ' 755 | ' Test with this script... 756 | ' 757 | ' s$ = "yo1" 758 | ' ss = "yo2" 759 | ' i% = 111 760 | ' ii = 222 761 | ' f! = 333.333 762 | ' ff = 444.444 763 | ' d# = 555.555 764 | ' dd = 555.555 765 | ' bb = true 766 | ' 767 | ' so = CreateObject("roString") 768 | ' so.SetString("strobj") 769 | ' io = CreateObject("roInt") 770 | ' io.SetInt(666) 771 | ' tm = CreateObject("roTimespan") 772 | ' 773 | ' Dbg("", s$ ) 'call the Dbg() function which calls AnyToString() 774 | ' Dbg("", ss ) 775 | ' Dbg("", "yo3") 776 | ' Dbg("", i% ) 777 | ' Dbg("", ii ) 778 | ' Dbg("", 2222 ) 779 | ' Dbg("", f! ) 780 | ' Dbg("", ff ) 781 | ' Dbg("", 3333.3333 ) 782 | ' Dbg("", d# ) 783 | ' Dbg("", dd ) 784 | ' Dbg("", so ) 785 | ' Dbg("", io ) 786 | ' Dbg("", bb ) 787 | ' Dbg("", true ) 788 | ' Dbg("", tm ) 789 | ' 790 | ' try to convert an object to a string. return invalid if can't 791 | '****************************************************** 792 | Function AnyToString(any As Dynamic) As Dynamic 793 | if (any = invalid) then 794 | return "invalid" 795 | end if 796 | if (isstr(any)) then 797 | return any 798 | end if 799 | if (isint(any)) then 800 | return itostr(any) 801 | end if 802 | if (isbool(any)) then 803 | if (any = true) then 804 | return "true" 805 | end if 806 | return "false" 807 | end if 808 | if (isfloat(any)) then 809 | return Str(any) 810 | end if 811 | if (type(any) = "roTimespan") then 812 | return itostr(any.TotalMilliseconds()) + "ms" 813 | end if 814 | return invalid 815 | End Function 816 | 817 | '****************************************************** 818 | ' Walk an XML tree and print it 819 | '****************************************************** 820 | Sub PrintXML(element As Object, depth As Integer) 821 | print tab(depth*3);"Name: [" + element.GetName() + "]" 822 | if (invalid <> element.GetAttributes()) then 823 | print tab(depth*3);"Attributes: "; 824 | for each a in element.GetAttributes() 825 | print a;"=";left(element.GetAttributes()[a], 4000); 826 | if (element.GetAttributes().IsNext()) then 827 | print ", "; 828 | end if 829 | next 830 | print 831 | end if 832 | 833 | if (element.GetBody() = invalid) then 834 | ' print tab(depth*3);"No Body" 835 | else if (type(element.GetBody()) = "roString") then 836 | print tab(depth*3);"Contains string: [" + left(element.GetBody(), 4000) + "]" 837 | else 838 | print tab(depth*3);"Contains list:" 839 | for each e in element.GetBody() 840 | PrintXML(e, depth+1) 841 | next 842 | end if 843 | print 844 | end sub 845 | 846 | '****************************************************** 847 | ' Dump the bytes of a string 848 | '****************************************************** 849 | Sub DumpString(str As String) 850 | print "DUMP STRING" 851 | print "---------------------------" 852 | print str 853 | print "---------------------------" 854 | l = Len(str)-1 855 | i = 0 856 | for i = 0 to l 857 | c = Mid(str, i) 858 | val = Asc(c) 859 | print itostr(val) 860 | next 861 | print "---------------------------" 862 | End Sub 863 | 864 | '****************************************************** 865 | ' Validate parameter is the correct type 866 | '****************************************************** 867 | Function validateParam(param As Object, paramType As String,functionName As String, allowInvalid = false) As Boolean 868 | if (type(param) = paramType) then 869 | return true 870 | end if 871 | 872 | if (allowInvalid = true) then 873 | if (type(param) = invalid) then 874 | return true 875 | end if 876 | end if 877 | 878 | print "invalid parameter of type "; type(param); " for "; paramType; " in function "; functionName 879 | return false 880 | End Function 881 | 882 | Function wrap(num As Integer, size As Dynamic) As Integer 883 | ' wraps via mod if size works 884 | ' else just clips negatives to zero 885 | ' (sort of an indefinite size wrap where we assume 886 | ' size is at least num and punt with negatives) 887 | remainder = num 888 | if (isint(size) and size <> 0) then 889 | base = int(num/size)*size 890 | remainder = num - base 891 | else if (num < 0) then 892 | remainder = 0 893 | end if 894 | return remainder 895 | End Function 896 | 897 | Function simpleJSONParser( jsonString As String ) As Object 898 | q = chr(34) 899 | 900 | beforeKey = "[,{]" 901 | keyFiller = "[^:]*?" 902 | keyNospace = "[-_\w\d]+" 903 | valueStart = "[" +q+ "\d\[{]|true|false|null" 904 | reReplaceKeySpaces = "("+beforeKey+")\s*"+q+"("+keyFiller+")("+keyNospace+")\s+("+keyNospace+")\s*"+q+"\s*:\s*(" + valueStart + ")" 905 | 906 | regexKeyUnquote = CreateObject( "roRegex", q + "([a-zA-Z0-9_\-\s]*)" + q + "\:", "i" ) 907 | regexKeyUnspace = CreateObject( "roRegex", reReplaceKeySpaces, "i" ) 908 | regexQuote = CreateObject( "roRegex", "\\" + q, "i" ) 909 | 910 | ' setup "null" variable 911 | null = invalid 912 | 913 | ' Replace escaped quotes 914 | jsonString = regexQuote.ReplaceAll( jsonString, q + " + q + " + q ) 915 | 916 | while (regexKeyUnspace.isMatch( jsonString )) 917 | jsonString = regexKeyUnspace.ReplaceAll( jsonString, "\1"+q+"\2\3\4"+q+": \5" ) 918 | end while 919 | 920 | jsonString = regexKeyUnquote.ReplaceAll( jsonString, "\1:" ) 921 | 922 | jsonObject = invalid 923 | ' Eval the BrightScript formatted JSON string 924 | Eval( "jsonObject = " + jsonString ) 925 | Return jsonObject 926 | End Function 927 | 928 | Function SimpleJSONBuilder( jsonArray As Object ) As String 929 | Return SimpleJSONAssociativeArray( jsonArray ) 930 | End Function 931 | 932 | 933 | Function SimpleJSONAssociativeArray( jsonArray As Object ) As String 934 | jsonString = "{" 935 | 936 | For Each key in jsonArray 937 | value = jsonArray[ key ] 938 | valType = type(value) 939 | if (valType <> "roInvalid") then 940 | jsonString = jsonString + Chr(34) + key + Chr(34) + ":" 941 | if (isstr(value)) then 942 | jsonString = jsonString + Chr(34) + value + Chr(34) 943 | else if (isint(value) OR isfloat(value)) then 944 | jsonString = jsonString + value.ToStr() 945 | else if (isbool(value)) then 946 | jsonString = jsonString + IIf( value, "true", "false" ) 947 | else if (valType = "roArray") then 948 | jsonString = jsonString + SimpleJSONArray( value ) 949 | else if (valType = "roAssociativeArray") then 950 | jsonString = jsonString + SimpleJSONBuilder( value ) 951 | else 952 | print("Unhandled type: " + type(value)) 953 | jsonString = jsonString + tostr(value) 954 | end If 955 | jsonString = jsonString + "," 956 | end if 957 | Next 958 | If Right( jsonString, 1 ) = "," Then 959 | jsonString = Left( jsonString, Len( jsonString ) - 1 ) 960 | End If 961 | 962 | jsonString = jsonString + "}" 963 | Return jsonString 964 | End Function 965 | 966 | 967 | Function SimpleJSONArray( jsonArray As Object ) As String 968 | jsonString = "[" 969 | 970 | For Each value in jsonArray 971 | valType = type(value) 972 | if (valType <> "roInvalid") then 973 | if (isstr(value)) then 974 | jsonString = jsonString + Chr(34) + value + Chr(34) 975 | else if (isint(value) OR isfloat(value)) then 976 | jsonString = jsonString + value.ToStr() 977 | else if (isbool(value)) then 978 | jsonString = jsonString + IIf( value, "true", "false" ) 979 | else if (valType = "roArray") then 980 | jsonString = jsonString + SimpleJSONArray( value ) 981 | else if (valType = "roAssociativeArray") then 982 | jsonString = jsonString + SimpleJSONBuilder( value ) 983 | else 984 | print("Unhandled type: " + type(value)) 985 | jsonString = jsonString + tostr(value) 986 | end if 987 | jsonString = jsonString + "," 988 | end if 989 | Next 990 | If Right( jsonString, 1 ) = "," Then 991 | jsonString = Left( jsonString, Len( jsonString ) - 1 ) 992 | End If 993 | 994 | jsonString = jsonString + "]" 995 | Return jsonString 996 | End Function 997 | 998 | Function IIf( Condition, Result1, Result2 ) 999 | If Condition Then 1000 | Return Result1 1001 | Else 1002 | Return Result2 1003 | End If 1004 | End Function -------------------------------------------------------------------------------- /source/video.brs: -------------------------------------------------------------------------------- 1 | 2 | Function LoadYouTube() As Object 3 | ' global singleton 4 | return m.youtube 5 | End Function 6 | 7 | Function InitYouTube() As Object 8 | ' constructor 9 | this = CreateObject("roAssociativeArray") 10 | this.device_id = CreateObject("roDeviceInfo").GetDeviceUniqueId() 11 | this.oauth_prefix = "https://www.google.com/accounts" 12 | this.link_prefix = "http://roku.toasterdesigns.net" 13 | this.devKey = "AI39si7xeR7W6rGgB9pZ3xBKHZnPVlBBdU3HZnhFXg8g7_3V8rplFNAT6rx_SVRzLRPhhNN-JARUjVg4JKGI5xjO00lK_Omb7g" 14 | this.protocol = "http" 15 | this.scope = this.protocol + "://gdata.youtube.com" 16 | this.prefix = this.scope + "/feeds/api" 17 | this.currentURL = "" 18 | this.searchLengthFilter = "" 19 | tmpLength = RegRead("length", "Search") 20 | if (tmpLength <> invalid) then 21 | this.searchLengthFilter = tmpLength 22 | end if 23 | this.searchDateFilter = "" 24 | tmpDate = RegRead("date", "Search") 25 | if (tmpDate <> invalid) then 26 | this.searchDateFilter = tmpDate 27 | end if 28 | 29 | this.searchSort = "" 30 | tmpSort = RegRead("sort", "Search") 31 | if (tmpSort <> invalid) then 32 | this.searchSort = tmpSort 33 | end if 34 | 35 | this.CurrentPageTitle = "" 36 | this.screen = invalid 37 | this.video = invalid 38 | 39 | ' Caches the latest video the user has watched 40 | ' This is used when sending out the video over the network 41 | this.activeVideo = invalid 42 | 43 | 'API Calls 44 | this.ExecServerAPI = youtube_exec_api 45 | 46 | 'Search 47 | this.SearchYouTube = youtube_search 48 | 49 | 'User videos 50 | this.BrowseUserVideos = youtube_user_videos 51 | 52 | ' Playlists 53 | this.BrowseUserPlaylists = BrowseUserPlaylists_impl 54 | 55 | 'related 56 | this.ShowRelatedVideos = youtube_related_videos 57 | 58 | 'Videos 59 | this.DisplayVideoListFromVideoList = DisplayVideoListFromVideoList_impl 60 | this.DisplayVideoListFromMetadataList = DisplayVideoListFromMetadataList_impl 61 | this.FetchVideoList = FetchVideoList_impl 62 | this.VideoDetails = VideoDetails_impl 63 | this.newVideoListFromXML = youtube_new_video_list 64 | this.newVideoFromXML = youtube_new_video 65 | this.ReturnVideoList = youtube_return_video 66 | 67 | 'Categories 68 | this.CategoriesListFromXML = CategoriesListFromXML_impl 69 | 70 | this.BuildButtons = BuildButtons_impl 71 | 72 | 'Settings 73 | this.BrowseSettings = youtube_browse_settings 74 | this.About = youtube_about 75 | this.AddAccount = youtube_add_account 76 | this.RedditSettings = EditRedditSettings 77 | this.ClearHistory = youtube_clear_history 78 | 79 | ' History 80 | this.ShowHistory = show_history 81 | this.AddHistory = add_history 82 | this.GetVideoObject = get_video_object 83 | this.GetVideoDetails = get_video_details 84 | 85 | this.udp_socket = invalid 86 | this.mp_socket = invalid 87 | 88 | ' Regex found on the internets here: http://stackoverflow.com/questions/3452546/javascript-regex-how-to-get-youtube-video-id-from-url 89 | ' Pre-compile the YouTube video ID regex 90 | this.ytIDRegex = CreateObject("roRegex", ".*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=)([^#\&\?]*).*", "") 91 | 92 | return this 93 | End Function 94 | 95 | 96 | Function youtube_exec_api(request As Dynamic, username = "default" As Dynamic) As Object 97 | 'oa = Oauth() 98 | 99 | if (username = invalid) then 100 | username = "" 101 | else 102 | username = "users/" + username + "/" 103 | end if 104 | 105 | method = "GET" 106 | url_stub = request 107 | postdata = invalid 108 | headers = { } 109 | 110 | if (type(request) = "roAssociativeArray") then 111 | if (request.url_stub <> invalid) then 112 | url_stub = request.url_stub 113 | end if 114 | if (request.postdata <> invalid) then 115 | postdata = request.postdata 116 | method = "POST" 117 | end if 118 | if (request.headers <> invalid) then 119 | headers = request.headers 120 | end if 121 | if (request.method <> invalid) then 122 | method = request.method 123 | end if 124 | end if 125 | 126 | ' Cache the current URL for refresh operations 127 | m.currentURL = url_stub 128 | 129 | if (Instr(0, url_stub, "http://") OR Instr(0, url_stub, "https://")) then 130 | http = NewHttp(url_stub) 131 | else 132 | http = NewHttp(m.prefix + "/" + username + url_stub) 133 | end if 134 | 135 | 'print "URL " + http.GetURL() 136 | 137 | 'if not headers.DoesExist("X-GData-Key") then headers.AddReplace("X-GData-Key", "key="+m.devKey) 138 | 'if not headers.DoesExist("GData-Version") then headers.AddReplace("GData-Version", "2") 139 | 140 | http.method = method 141 | http.AddParam("v","2","urlParams") 142 | 'oa.sign(http,true) 143 | 144 | 'print "----------------------------------" 145 | if (Instr(1, request, "pkg:/") > 0) then 146 | rsp = ReadAsciiFile(request) 147 | else if (postdata <> invalid) then 148 | rsp = http.PostFromStringWithTimeout(postdata, 10, headers) 149 | 'print "postdata:",postdata 150 | else 151 | rsp = http.getToStringWithTimeout(10, headers) 152 | end if 153 | 154 | 155 | 'print "----------------------------------" 156 | 'print rsp 157 | 'print "----------------------------------" 158 | 159 | xml = ParseXML(rsp) 160 | 161 | returnObj = CreateObject("roAssociativeArray") 162 | returnObj.xml = xml 163 | returnObj.status = http.status 164 | if (Instr(1, request, "pkg:/") < 0) then 165 | returnObj.error = handleYoutubeError(returnObj) 166 | end if 167 | 168 | return returnObj 169 | End Function 170 | 171 | Function handleYoutubeError(rsp) As Dynamic 172 | ' Is there a status code? If not, return a connection error. 173 | if (rsp.status = invalid) then 174 | return ShowConnectionFailed() 175 | end if 176 | ' Don't check for errors if the response code was a 2xx or 3xx number 177 | if (int(rsp.status / 100) = 2 OR int(rsp.status / 100) = 3) then 178 | return "" 179 | end if 180 | 181 | if (not(isxmlelement(rsp.xml))) then 182 | return ShowErrorDialog("API return invalid. Try again later", "Bad response") 183 | end if 184 | 185 | error = rsp.xml.GetNamedElements("error")[0] 186 | if (error = invalid) then 187 | ' we got an unformatted HTML response with the error in the title 188 | error = rsp.xml.GetChildElements()[0].GetChildElements()[0].GetText() 189 | else 190 | error = error.GetNamedElements("internalReason")[0].GetText() 191 | end if 192 | 193 | ShowDialog1Button("Error", error, "OK", true) 194 | return error 195 | End Function 196 | 197 | '******************************************************************** 198 | ' YouTube User uploads 199 | '******************************************************************** 200 | Sub youtube_user_videos(username As String, userID As String) 201 | m.FetchVideoList("users/"+userID+"/uploads?orderby=published", "Videos By "+username, invalid) 202 | End Sub 203 | 204 | '******************************************************************** 205 | ' YouTube User Playlists 206 | '******************************************************************** 207 | Sub BrowseUserPlaylists_impl(username As String, userID As String) 208 | m.FetchVideoList("users/" + userID + "/playlists?max-results=50", username + "'s Playlists", invalid, true) 209 | End Sub 210 | 211 | '******************************************************************** 212 | ' YouTube Related Videos 213 | '******************************************************************** 214 | Sub youtube_related_videos(video As Object) 215 | m.FetchVideoList("videos/"+ video.id +"/related?v=2", "Related Videos", invalid) 216 | 'GetYTBase("videos/" + showList[showIndex].ContentId + "/related?v=2&start-index=1&max-results=50")) 217 | End Sub 218 | 219 | '******************************************************************** 220 | ' YouTube Poster/Video List Utils 221 | '******************************************************************** 222 | Sub FetchVideoList_impl(APIRequest As Dynamic, title As String, username As Dynamic, categories=false, message = "Loading..." as String) 223 | 224 | 'fields = m.FieldsToInclude 225 | 'if Instr(0, APIRequest, "?") = 0 then 226 | ' fields = "?"+Mid(fields, 2) 227 | 'end if 228 | 229 | screen = uitkPreShowPosterMenu("flat-episodic-16x9", title) 230 | screen.showMessage(message) 231 | 232 | response = m.ExecServerAPI(APIRequest, username) 233 | if (response.status = 403) then 234 | ShowErrorDialog(title + " may be private, or unavailable at this time. Try again.", "403 Forbidden") 235 | return 236 | end if 237 | if (not(isxmlelement(response.xml))) then 238 | ShowConnectionFailed() 239 | return 240 | end if 241 | 242 | ' Everything is OK, display the list 243 | xml = response.xml 244 | if (categories = true) then 245 | categories = m.CategoriesListFromXML(xml.entry) 246 | 'PrintAny(0, "categoryList:", categories) 247 | m.DisplayVideoListFromVideoList([], title, xml.link, screen, categories) 248 | else 249 | videos = m.newVideoListFromXML(xml.entry) 250 | m.DisplayVideoListFromVideoList(videos, title, xml.link, screen, invalid) 251 | end if 252 | End Sub 253 | 254 | 255 | Function youtube_return_video(APIRequest As Dynamic, title As String, username As Dynamic) 256 | xml = m.ExecServerAPI(APIRequest, username)["xml"] 257 | if (not(isxmlelement(xml))) then 258 | ShowConnectionFailed() 259 | return [] 260 | end if 261 | 262 | videos = m.newVideoListFromXML(xml.entry) 263 | metadata = GetVideoMetaData(videos) 264 | 265 | if (xml.link <> invalid) then 266 | for each link in xml.link 267 | if (link@rel = "next") then 268 | metadata.Push({shortDescriptionLine1: "More Results", action: "next", pageURL: link@href, HDPosterUrl:"pkg:/images/icon_next_episode.jpg", SDPosterUrl:"pkg:/images/icon_next_episode.jpg"}) 269 | else if (link@rel = "previous") then 270 | metadata.Unshift({shortDescriptionLine1: "Back", action: "prev", pageURL: link@href, HDPosterUrl:"pkg:/images/icon_prev_episode.jpg", SDPosterUrl:"pkg:/images/icon_prev_episode.jpg"}) 271 | end if 272 | end for 273 | end if 274 | 275 | return metadata 276 | End Function 277 | 278 | Sub DisplayVideoListFromVideoList_impl(videos As Object, title As String, links=invalid, screen = invalid, categories = invalid, metadataFunc = GetVideoMetaData as Function) 279 | if (categories = invalid) then 280 | metadata = metadataFunc(videos) 281 | else 282 | metadata = videos 283 | end if 284 | m.DisplayVideoListFromMetadataList(metadata, title, links, screen, categories) 285 | End Sub 286 | 287 | Sub DisplayVideoListFromMetadataList_impl(metadata As Object, title As String, links=invalid, screen = invalid, categories = invalid) 288 | if (screen = invalid) then 289 | screen = uitkPreShowPosterMenu("flat-episodic-16x9", title) 290 | screen.showMessage("Loading...") 291 | end if 292 | m.CurrentPageTitle = title 293 | 294 | if (categories <> invalid) then 295 | categoryList = CreateObject("roArray", 100, true) 296 | for each category in categories 297 | categoryList.Push(category.title) 298 | next 299 | 300 | oncontent_callback = [categories, m, 301 | function(categories, youtube, set_idx) 302 | 'PrintAny(0, "category:", categories[set_idx]) 303 | if (youtube <> invalid AND categories.Count() > 0) then 304 | return youtube.ReturnVideoList(categories[set_idx].link, youtube.CurrentPageTitle, invalid) 305 | else 306 | return [] 307 | end if 308 | end function] 309 | 310 | 311 | onclick_callback = [categories, m, 312 | function(categories, youtube, video, category_idx, set_idx) 313 | if (video[set_idx]["action"] <> invalid) then 314 | return { isContentList: true, content: youtube.ReturnVideoList(video[set_idx]["pageURL"], youtube.CurrentPageTitle, invalid) } 315 | else 316 | youtube.VideoDetails(video[set_idx], youtube.CurrentPageTitle, video, set_idx) 317 | return { isContentList: false, content: video} 318 | end if 319 | end function] 320 | uitkDoCategoryMenu(categoryList, screen, oncontent_callback, onclick_callback, onplay_callback) 321 | else if (metadata.Count() > 0) then 322 | for each link in links 323 | if (type(link) = "roXMLElement") then 324 | if (link@rel = "next") then 325 | metadata.Push({shortDescriptionLine1: "More Results", action: "next", pageURL: link@href, HDPosterUrl:"pkg:/images/icon_next_episode.jpg", SDPosterUrl:"pkg:/images/icon_next_episode.jpg"}) 326 | else if (link@rel = "previous") then 327 | metadata.Unshift({shortDescriptionLine1: "Back", action: "prev", pageURL: link@href, HDPosterUrl:"pkg:/images/icon_prev_episode.jpg", SDPosterUrl:"pkg:/images/icon_prev_episode.jpg"}) 328 | end if 329 | else if (type(link) = "roAssociativeArray") then 330 | if (link.type = "next") then 331 | metadata.Push({shortDescriptionLine1: "More Results", action: "next", pageURL: link.href, HDPosterUrl:"pkg:/images/icon_next_episode.jpg", SDPosterUrl:"pkg:/images/icon_next_episode.jpg", func: link.func}) 332 | else if (link.type = "previous") then 333 | metadata.Unshift({shortDescriptionLine1: "Back", action: "prev", pageURL: link.href, HDPosterUrl:"pkg:/images/icon_prev_episode.jpg", SDPosterUrl:"pkg:/images/icon_prev_episode.jpg", func: link.func}) 334 | end if 335 | end if 336 | end for 337 | 338 | onselect = [1, metadata, m, 339 | function(video, youtube, set_idx) 340 | if (video[set_idx]["func"] <> invalid) then 341 | video[set_idx]["func"](youtube, video[set_idx]["pageURL"]) 342 | else if (video[set_idx]["action"] <> invalid) then 343 | youtube.FetchVideoList(video[set_idx]["pageURL"], youtube.CurrentPageTitle, invalid) 344 | else 345 | youtube.VideoDetails(video[set_idx], youtube.CurrentPageTitle, video, set_idx) 346 | end if 347 | end function] 348 | uitkDoPosterMenu(metadata, screen, onselect, onplay_callback) 349 | else 350 | uitkDoMessage("No videos found.", screen) 351 | end if 352 | End Sub 353 | 354 | '******************************************************************** 355 | ' Callback function for when the user hits the play button from the video list 356 | ' screen. 357 | ' @param theVideo the video metadata object that should be played. 358 | '******************************************************************** 359 | Sub onplay_callback(theVideo as Object) 360 | result = video_get_qualities(theVideo) 361 | if (result = 0) then 362 | DisplayVideo(theVideo) 363 | end if 364 | End Sub 365 | 366 | '******************************************************************** 367 | ' Creates the list of categories from the provided XML 368 | ' @param xmlList the XML to create the category list from. 369 | ' @return an roList, which will be sorted by the yt:unreadCount if the XML 370 | ' represents a list of subscriptions. 371 | ' each category has the following members: 372 | ' title 373 | ' link 374 | '******************************************************************** 375 | Function CategoriesListFromXML_impl(xmlList As Object) As Object 376 | 'print "CategoriesListFromXML_impl init" 377 | categoryList = CreateObject("roList") 378 | for each record in xmlList 379 | ''printAny(0, "xmlList:", record) 380 | category = CreateObject("roAssociativeArray") 381 | category.title = record.GetNamedElements("title").GetText() 382 | category.link = validstr(record.content@src) 383 | 384 | if (record.GetNamedElements("yt:unreadCount").Count() > 0) then 385 | category.unreadCount% = record.GetNamedElements("yt:unreadCount").GetText().toInt() 386 | else 387 | category.unreadCount% = 0 388 | end if 389 | ' print (category.title + " unreadCount: " + tostr(category.unreadCount%)) 390 | 391 | if (isnullorempty(category.link)) then 392 | links = record.link 393 | for each link in links 394 | if (Instr(1, link@rel, "user.uploads") > 0) then 395 | category.link = validstr(link@href) + "&max-results=50" 396 | end if 397 | next 398 | end if 399 | 400 | categoryList.Push(category) 401 | next 402 | Sort(categoryList, Function(obj as Object) as Integer 403 | return obj.unreadCount% 404 | End Function) 405 | return categoryList 406 | End Function 407 | 408 | 409 | 410 | '******************************************************************** 411 | ' Creates a list of video metadata objects from the provided XML 412 | ' @param xmlList the XML to create the list of videos from 413 | ' @return an roList of video metadata objects 414 | '******************************************************************** 415 | Function youtube_new_video_list(xmlList As Object) As Object 416 | 'print "youtube_new_video_list init" 417 | videolist = CreateObject("roList") 418 | for each record in xmlList 419 | video = m.newVideoFromXML(record) 420 | videolist.Push(video) 421 | next 422 | return videolist 423 | End Function 424 | 425 | Function youtube_new_video(xml As Object) As Object 426 | video = CreateObject("roAssociativeArray") 427 | video.youtube = m 428 | video.xml = xml 429 | video.GetID = get_xml_id 430 | video.GetAuthor = get_xml_author 431 | video.GetUserID = function():return m.xml.GetNamedElements("media:group")[0].GetNamedElements("yt:uploaderId")[0].GetText():end function 432 | video.GetTitle = function():return m.xml.title[0].GetText():end function 433 | video.GetCategory = function():return m.xml.GetNamedElements("media:group")[0].GetNamedElements("media:category")[0].GetText():end function 434 | video.GetDesc = get_desc 435 | video.GetLength = GetLength_impl 436 | video.GetUploadDate = GetUploadDate_impl 437 | video.GetRating = get_xml_rating 438 | video.GetThumb = get_xml_thumb 439 | 'video.GetLinks = function():return m.xml.GetNamedElements("link"):end function 440 | 'video.GetURL = video_get_url 441 | return video 442 | End Function 443 | 444 | Function GetVideoMetaData(videos As Object) 445 | metadata = [] 446 | 447 | for each video in videos 448 | meta = CreateObject("roAssociativeArray") 449 | meta.ContentType = "movie" 450 | meta["ID"] = video.GetID() 451 | meta["Author"] = video.GetAuthor() 452 | meta["TitleSeason"] = video.GetTitle() 453 | meta["Title"] = video.GetAuthor() + " - " + get_length_as_human_readable(video.GetLength()) 454 | meta["Actors"] = meta.Author 455 | meta["Description"] = video.GetDesc() 456 | meta["Categories"] = video.GetCategory() 457 | meta["StarRating"] = video.GetRating() 458 | meta["ShortDescriptionLine1"] = meta.TitleSeason 459 | meta["ShortDescriptionLine2"] = meta.Title 460 | meta["SDPosterUrl"] = video.GetThumb() 461 | meta["HDPosterUrl"] = video.GetThumb() 462 | meta["Length"] = video.GetLength().toInt() 463 | meta["xml"] = video.xml 464 | meta["UserID"] = video.GetUserID() 465 | meta["ReleaseDate"] = video.GetUploadDate() 466 | meta["StreamFormat"] = "mp4" 467 | meta["Live"] = false 468 | meta["Streams"] = [] 469 | meta["PlayStart"] = 0 470 | meta["SwitchingStrategy"] = "no-adaptation" 471 | 'meta.StreamBitrates=[] 472 | 'meta.StreamQualities=[] 473 | 'meta.StreamUrls=[] 474 | 475 | metadata.Push(meta) 476 | end for 477 | 478 | return metadata 479 | End Function 480 | 481 | Function get_desc() As Dynamic 482 | desc = m.xml.GetNamedElements("media:group")[0].GetNamedElements("media:description") 483 | if (desc.Count() > 0) then 484 | return Left(desc[0].GetText(), 300) 485 | end if 486 | return invalid 487 | End Function 488 | 489 | '******************************************* 490 | ' Returns the length of the video from the yt:duration element: 491 | ' 492 | '******************************************* 493 | Function GetLength_impl() As Dynamic 494 | durations = m.xml.GetNamedElements("media:group")[0].GetNamedElements("yt:duration") 495 | if (durations.Count() > 0) then 496 | return durations.GetAttributes()["seconds"] 497 | end if 498 | return "0" 499 | End Function 500 | 501 | '******************************************* 502 | ' Returns the date the video was uploaded, from the yt:uploaded element: 503 | ' val 504 | '******************************************* 505 | Function GetUploadDate_impl() As Dynamic 506 | uploaded = m.xml.GetNamedElements("media:group")[0].GetNamedElements("yt:uploaded") 507 | if (uploaded.Count() > 0) then 508 | dateText = uploaded.GetText() 509 | 'dateObj = CreateObject("roDateTime") 510 | ' The value from YouTube has a 'Z' at the end, we need to strip this off, or else 511 | ' FromISO8601String() can't parse the date properly 512 | 'dateObj.FromISO8601String(Left(dateText, Len(dateText) - 1)) 513 | 'return tostr(dateObj.GetMonth()) + "/" + tostr(dateObj.GetDayOfMonth()) + "/" + tostr(dateObj.GetYear()) 514 | return Left(dateText, 10) 515 | end if 516 | return "" 517 | End Function 518 | 519 | '******************************************* 520 | ' Returns the length of the video in a human-friendly format 521 | ' i.e. 3700 seconds becomes: 1h 1m 522 | ' TODO: use utility functions in generalUtils 523 | '******************************************* 524 | Function get_length_as_human_readable(length As Dynamic) As String 525 | if (type(length) = "roString") then 526 | len% = length.ToInt() 527 | else if (type(length) = "roInteger") then 528 | len% = length 529 | else 530 | return "Unknown" 531 | end if 532 | 533 | if ( len% > 0 ) then 534 | hours% = FIX(len% / 3600) 535 | len% = len% - (hours% * 3600) 536 | minutes% = FIX(len% / 60) 537 | seconds% = len% MOD 60 538 | if ( hours% > 0 ) then 539 | return Stri(hours%) + "h" + Stri(minutes%) + "m" 540 | else 541 | return Stri(minutes%) + "m" + Stri(seconds%) + "s" 542 | end if 543 | else if ( len% = 0 ) then 544 | return "Live Stream" 545 | end if 546 | ' Default return 547 | return "Unknown" 548 | End Function 549 | 550 | Function get_xml_id() As Dynamic 551 | videoid=m.xml.GetNamedElements("media:group")[0].GetNamedElements("yt:videoid") 552 | if (videoid<>invalid and videoid.Count() > 0) then 553 | return videoid[0].GetText() 554 | end if 555 | End Function 556 | 557 | Function get_xml_author() As Dynamic 558 | credits=m.xml.GetNamedElements("media:group")[0].GetNamedElements("media:credit") 559 | if (credits<>invalid and credits.Count() > 0) then 560 | for each author in credits 561 | if (author.GetAttributes()["role"] = "uploader") then 562 | return author.GetAttributes()["yt:display"] 563 | end if 564 | end for 565 | end if 566 | End Function 567 | 568 | Function get_xml_rating() As Dynamic 569 | if (m.xml.GetNamedElements("gd:rating").Count() > 0) then 570 | return Int(m.xml.GetNamedElements("gd:rating").GetAttributes()["average"].toFloat() * 20) 571 | end if 572 | return invalid 573 | End Function 574 | 575 | Function get_xml_thumb() As Dynamic 576 | thumbs=m.xml.GetNamedElements("media:group")[0].GetNamedElements("media:thumbnail") 577 | if (thumbs.Count() > 0) then 578 | for each thumb in thumbs 579 | if (thumb.GetAttributes()["yt:name"] = "mqdefault") then 580 | return thumb.GetAttributes()["url"] 581 | end if 582 | end for 583 | return m.xml.GetNamedElements("media:group")[0].GetNamedElements("media:thumbnail")[0].GetAttributes()["url"] 584 | end if 585 | return "pkg:/images/icon_s.jpg" 586 | End Function 587 | 588 | 589 | '******************************************************************** 590 | ' YouTube video details roSpringboardScreen 591 | '******************************************************************** 592 | Sub VideoDetails_impl(theVideo As Object, breadcrumb As String, videos=invalid, idx=invalid) 593 | p = CreateObject("roMessagePort") 594 | screen = CreateObject("roSpringboardScreen") 595 | screen.SetMessagePort(p) 596 | 597 | ' If it is history get Details from ID 598 | if (theVideo["IsHistory"] <> invalid) then 599 | theVideo = m.GetVideoDetails(theVideo) 600 | end if 601 | 602 | m.screen = screen 603 | m.video = theVideo 604 | screen.SetDescriptionStyle("movie") 605 | if (theVideo.StarRating = invalid) then 606 | screen.SetStaticRatingEnabled(false) 607 | end if 608 | if (videos.Count() > 1) then 609 | screen.AllowNavLeft(true) 610 | screen.AllowNavRight(true) 611 | end if 612 | screen.SetPosterStyle("rounded-rect-16x9-generic") 613 | screen.SetDisplayMode("zoom-to-fill") 614 | screen.SetBreadcrumbText(breadcrumb, "Video") 615 | 616 | buttons = m.BuildButtons() 617 | 618 | screen.SetContent(m.video) 619 | screen.Show() 620 | 621 | while (true) 622 | msg = wait(2000, screen.GetMessagePort()) 623 | if (type(msg) = "roSpringboardScreenEvent") then 624 | if (msg.isScreenClosed()) then 625 | 'print "Closing springboard screen" 626 | exit while 627 | else if (msg.isButtonPressed()) then 628 | 'print "Button pressed: "; msg.GetIndex(); " " msg.GetData() 629 | if (msg.GetIndex() = 0) then ' Play/Resume 630 | result = video_get_qualities(m.video) 631 | if (result = 0) then 632 | DisplayVideo(m.video) 633 | buttons = m.BuildButtons() 634 | end if 635 | else if (msg.GetIndex() = 5) then ' Play from beginning 636 | m.video["PlayStart"] = 0 637 | result = video_get_qualities(m.video) 638 | if (result = 0) then 639 | DisplayVideo(m.video) 640 | buttons = m.BuildButtons() 641 | end if 642 | else if (msg.GetIndex() = 1) then ' Play All 643 | for i = idx to videos.Count() - 1 Step +1 644 | selectedVideo = videos[i] 645 | result = video_get_qualities(selectedVideo) 646 | if (result = 0) then 647 | ret = DisplayVideo(selectedVideo) 648 | if (ret > 0) then 649 | buttons = m.BuildButtons() 650 | Exit For 651 | end if 652 | end if 653 | end for 654 | else if (msg.GetIndex() = 2) then 655 | m.ShowRelatedVideos(m.video) 656 | else if (msg.GetIndex() = 3) then 657 | m.BrowseUserVideos(m.video.Author, m.video.UserID) 658 | else if (msg.GetIndex() = 4) then 659 | m.BrowseUserPlaylists(m.video.Author, m.video.UserID) 660 | end if 661 | else if (msg.isRemoteKeyPressed()) then 662 | if (msg.GetIndex() = 4) then ' left 663 | if (videos.Count() > 1) then 664 | idx = idx - 1 665 | if ( idx < 0 ) then 666 | ' Last video is the 'next' video link 667 | idx = videos.Count() - 2 668 | end if 669 | m.video = videos[idx] 670 | buttons = m.BuildButtons() 671 | screen.SetContent( m.video ) 672 | end if 673 | else if (msg.GetIndex() = 5) then ' right 674 | if (videos.Count() > 1) then 675 | idx = idx + 1 676 | if ( idx = videos.Count() - 1 ) then 677 | ' Last video is the 'next' video link 678 | idx = 0 679 | end if 680 | m.video = videos[idx] 681 | buttons = m.BuildButtons() 682 | screen.SetContent( m.video ) 683 | end if 684 | end if 685 | else 686 | 'print "Unknown event: "; msg.GetType(); " msg: "; msg.GetMessage() 687 | end if 688 | else if (msg = invalid) then 689 | CheckForMCast() 690 | end If 691 | end while 692 | End Sub 693 | 694 | '******************************************************************** 695 | ' Helper function to build the list of buttons on the springboard 696 | ' @return an roAssociativeArray of the buttons 697 | '******************************************************************** 698 | Function BuildButtons_impl() as Object 699 | m.screen.ClearButtons() 700 | buttons = CreateObject("roAssociativeArray") 701 | resumeEnabled = false 702 | if (m.video.Live = false AND m.video.PlayStart > 0) then 703 | resumeEnabled = true 704 | buttons["resume"] = m.screen.AddButton(0, "Resume") 705 | buttons["restart"] = m.screen.AddButton(5, "Play from beginning") 706 | else 707 | buttons["play"] = m.screen.AddButton(0, "Play") 708 | end if 709 | buttons["play_all"] = m.screen.AddButton(1, "Play All") 710 | if (m.video.Author <> invalid) then 711 | ' Hide related videos if the Resume/Play from beginning options are enabled 712 | if (not(resumeEnabled)) then 713 | buttons["show_related"] = m.screen.AddButton(2, "Show Related Videos") 714 | end if 715 | buttons["more"] = m.screen.AddButton(3, "More Videos By " + m.video.Author) 716 | buttons["playlists"] = m.screen.AddButton(4, "Show "+ m.video.Author + "'s playlists") 717 | end if 718 | return buttons 719 | End Function 720 | 721 | '******************************************************************** 722 | ' The video playback screen 723 | '******************************************************************** 724 | Function DisplayVideo(content As Object) 725 | p = CreateObject("roMessagePort") 726 | video = CreateObject("roVideoScreen") 727 | video.setMessagePort(p) 728 | video.SetPositionNotificationPeriod(5) 729 | 730 | yt = LoadYouTube() 731 | 732 | ' Cache the video information for network sharing 733 | yt.activeVideo = content 734 | 735 | video.SetContent(content) 736 | video.show() 737 | 738 | yt.AddHistory(content) 739 | ret = -1 740 | while (true) 741 | msg = wait(0, video.GetMessagePort()) 742 | if (type(msg) = "roVideoScreenEvent") then 743 | if (Instr(1, msg.getMessage(), "interrupted") > 0) then 744 | ret = 1 745 | end if 746 | if (msg.isScreenClosed()) then 'ScreenClosed event 747 | 'print "Closing video screen" 748 | video.Close() 749 | exit while 750 | else if (msg.isRequestFailed()) then 751 | 'print "play failed: "; msg.GetMessage() 752 | else if (msg.isPlaybackPosition()) then 753 | content["PlayStart"] = msg.GetIndex() 754 | else if (msg.isFullResult()) then 755 | content["PlayStart"] = 0 756 | ' The video has completed, zero out the cached version 757 | yt.activeVideo = invalid 758 | else if (msg.isPartialResult()) then 759 | ' For plugin videos, the Length may not be available. 760 | if (content.Length <> invalid and content.PlayStart <> invalid) then 761 | ' If we're within 30 seconds of the end of the video, don't allow resume 762 | if (content.PlayStart > (content.Length - 30)) then 763 | content["PlayStart"] = 0 764 | ' The video has completed, zero out the cached version 765 | yt.activeVideo = invalid 766 | end if 767 | end if 768 | ' Else if the length isn't valid, always allow resume 769 | else 770 | 'print "Unknown event: "; msg.GetType(); " msg: "; msg.GetMessage() 771 | end if 772 | end if 773 | end while 774 | return ret 775 | End Function 776 | 777 | function getMP4Url(video as Object, timeout = 0 as integer, loginCookie = "" as string) as object 778 | video.Streams.Clear() 779 | if (Left(LCase(video.id), 4) = "http") then 780 | url = video.id 781 | else 782 | url = "http://www.youtube.com/get_video_info?hl=en&el=detailpage&video_id=" + video.id 783 | end if 784 | htmlString = "" 785 | port = CreateObject("roMessagePort") 786 | ut = CreateObject("roUrlTransfer") 787 | ut.SetPort(port) 788 | ut.AddHeader("User-Agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)") 789 | ut.AddHeader("Cookie", loginCookie) 790 | ut.SetUrl(url) 791 | if (ut.AsyncGetToString()) then 792 | while (true) 793 | msg = Wait(timeout, port) 794 | if (type(msg) = "roUrlEvent") then 795 | status = msg.GetResponseCode() 796 | if (status = 200) then 797 | htmlString = msg.GetString() 798 | end if 799 | exit while 800 | else if (type(msg) = "Invalid") then 801 | ut.AsyncCancel() 802 | exit while 803 | end if 804 | end while 805 | end if 806 | urlEncodedFmtStreamMap = CreateObject("roRegex", "url_encoded_fmt_stream_map=([^(" + Chr(34) + "|&|$)]*)", "").Match(htmlString) 807 | if (urlEncodedFmtStreamMap.Count() > 1) then 808 | if (not(strTrim(urlEncodedFmtStreamMap[1]) = "")) then 809 | commaSplit = CreateObject("roRegex", "%2C", "").Split(urlEncodedFmtStreamMap [1]) 810 | for each commaItem in commaSplit 811 | pair = {itag: "", url: "", sig: ""} 812 | ampersandSplit = CreateObject("roRegex", "%26", "").Split(commaItem) 813 | for each ampersandItem in ampersandSplit 814 | equalsSplit = CreateObject("roRegex", "%3D", "").Split(ampersandItem) 815 | if (equalsSplit.Count() = 2) then 816 | pair[equalsSplit [0]] = equalsSplit [1] 817 | end if 818 | end for 819 | if (pair.url <> "" and Left(LCase(pair.url), 4) = "http") then 820 | if (pair.sig <> "") then 821 | signature = "&signature=" + pair.sig 822 | else 823 | signature = "" 824 | end if 825 | urlDecoded = ut.Unescape(ut.Unescape(pair.url + signature)) 826 | ' print "urlDecoded: " ; urlDecoded 827 | ' Determined from here: http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs 828 | if (pair.itag = "18") then 829 | ' 18 is MP4 270p/360p H.264 at .5 Mbps video bitrate 830 | video.Streams.Push({url: urlDecoded, bitrate: 512, quality: false, contentid: pair.itag}) 831 | else if (pair.itag = "22") then 832 | ' 22 is MP4 720p H.264 at 2-2.9 Mbps video bitrate. I set the bitrate to the maximum, for best results. 833 | video.Streams.Push({url: urlDecoded, bitrate: 2969, quality: true, contentid: pair.itag}) 834 | else if (pair.itag = "37") then 835 | ' 37 is MP4 1080p H.264 at 3-5.9 Mbps video bitrate. I set the bitrate to the maximum, for best results. 836 | video.Streams.Push({url: urlDecoded, bitrate: 6041, quality: true, contentid: pair.itag }) 837 | end if 838 | end if 839 | end for 840 | if (video.Streams.Count() > 0) then 841 | video.Live = false 842 | video.StreamFormat = "mp4" 843 | 'video["PlayStart"] = 0 844 | end if 845 | else 846 | hlsUrl = CreateObject("roRegex", "hlsvp=([^(" + Chr(34) + "|&|$)]*)", "").Match(htmlString) 847 | if (urlEncodedFmtStreamMap.Count() > 1) then 848 | urlDecoded = ut.Unescape(ut.Unescape(ut.Unescape(hlsUrl[1]))) 849 | 'print "Found hlsVP: " ; urlDecoded 850 | video.Streams.Clear() 851 | video.Live = true 852 | ' Set the PlayStart sufficiently large so it starts at 'Live' position 853 | video["PlayStart"] = 500000 854 | video.StreamFormat = "hls" 855 | 'video.SwitchingStrategy = "unaligned-segments" 856 | video.SwitchingStrategy = "minimum-adaptation" 857 | video.Streams.Push({url: urlDecoded, bitrate: 0, quality: false, contentid: -1}) 858 | end if 859 | 860 | end if 861 | else 862 | print ("Nothing in urlEncodedFmtStreamMap") 863 | end if 864 | return video.Streams 865 | end function 866 | 867 | 868 | Function video_get_qualities(video as Object) As Integer 869 | 870 | getMP4Url(video) 871 | if (video.Streams.Count() > 0) then 872 | return 0 873 | end if 874 | problem = ShowDialogNoButton("", "Having trouble finding a Roku-compatible stream...") 875 | sleep(3000) 876 | problem.Close() 877 | return -1 878 | End Function 879 | 880 | '******************************************************************** 881 | ' Shows Users Video History 882 | '******************************************************************** 883 | Sub show_history() 884 | videolist = CreateObject("roList") 885 | videosJSON = RegRead("videos", "history") 886 | if (videosJSON<>invalid) and (isnonemptystr(videosJSON)) then 887 | 'print "videosJSON :" + videosJSON 888 | history = simpleJSONParser(videosJSON) 889 | if (islist(history) = true) then 890 | for each video in history 891 | v = m.GetVideoObject(video) 892 | if (v<>invalid) then 893 | videolist.Push(v) 894 | end if 895 | end for 896 | end if 897 | end if 898 | 899 | m.DisplayVideoListFromMetadataList(videolist, "History", invalid, invalid, invalid) 900 | End Sub 901 | 902 | '******************************************************************** 903 | ' Adds Video to History 904 | ' We only store selected properties to save Memory in Registry 905 | '******************************************************************** 906 | Sub add_history(video as Object) 907 | videosJSON = RegRead("videos", "history") 908 | histObj = CreateObject("roAssociativeArray") 909 | 910 | ' Try to save some memory with one char property names 911 | histObj["I"] = video.ID 912 | histObj["T"] = video.ShortDescriptionLine1 913 | 914 | saved = false 915 | if (videosJSON<>invalid) and (isnonemptystr(videosJSON)) then 916 | history = simpleJSONParser(videosJSON) 917 | if (islist(history) = true) then 918 | j = 0 919 | k = -1 920 | for each v in history 921 | if v.i = histObj["I"] then 922 | k = j 923 | end if 924 | j = j + 1 925 | end for 926 | 927 | if k <> -1 then 928 | history.delete(k) 929 | end If 930 | 931 | 'Is it safe to assume that 50 items will be less than 16KB? Need to find how to check array size in bytes in brightscript 932 | if(history.Count() > 50) Then 933 | RegWrite.Shift() 934 | end If 935 | 936 | history.Unshift(histObj) 937 | RegWrite("videos", SimpleJSONArray(history), "history") 938 | saved = true 939 | end if 940 | end if 941 | 942 | if (not(saved)) then 943 | history = CreateObject("roArray", 1, true) 944 | history.Unshift(histObj) 945 | RegWrite("videos", SimpleJSONArray(history), "history") 946 | end if 947 | End Sub 948 | 949 | 950 | Function get_video_details(theVideo as Object) As Object 951 | api = "videos/" + tostr(theVideo["ID"]) + "?v=2" 952 | xml = m.ExecServerAPI(api, invalid)["xml"] 953 | if (isxmlelement(xml)) then 954 | video = m.newVideoFromXML(xml) 955 | videos = CreateObject("roArray", 1, true) 956 | videos.Push(video) 957 | metadata = GetVideoMetaData(videos) 958 | if (metadata <> invalid AND metadata.Count() > 0) then 959 | metadata[0].["ID"] = theVideo["ID"] 960 | theVideo = metadata[0] 961 | end if 962 | end if 963 | return theVideo 964 | End Function 965 | 966 | '******************************************************************** 967 | ' Builds Video Object based on History JSON Object 968 | ' TODO: This should be merged with GetVideoMetaData Function 969 | '******************************************************************** 970 | Function get_video_object(video as Object) As Object 971 | meta = CreateObject("roAssociativeArray") 972 | meta.ContentType = "movie" 973 | meta["ID"] = video.I 974 | meta["Author"] = "" 975 | meta["TitleSeason"] = "" 976 | meta["Title"] = "" 977 | meta["Actors"] = "" 978 | meta["Description"] = "" 979 | meta["Categories"] = "" 980 | meta["StarRating"] = "" 981 | meta["ShortDescriptionLine1"] = video.T 982 | meta["ShortDescriptionLine2"] = "" 983 | meta["SDPosterUrl"] = "http://img.youtube.com/vi/" + tostr(video.I) + "/mqdefault.jpg" 984 | meta["HDPosterUrl"] = meta["SDPosterUrl"] 985 | meta["Length"] = "" 986 | meta["xml"] = "" 987 | meta["UserID"] = "" 988 | meta["ReleaseDate"] = "" 989 | meta["StreamFormat"] = "mp4" 990 | meta["Live"] = false 991 | meta["Streams"] = [] 992 | meta["PlayStart"] = 0 993 | meta["SwitchingStrategy"] = "no-adaptation" 994 | meta["IsHistory"] = true 995 | 'meta.StreamBitrates=[] 996 | 'meta.StreamQualities=[] 997 | 'meta.StreamUrls=[] 998 | return meta 999 | End Function --------------------------------------------------------------------------------