├── 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
--------------------------------------------------------------------------------