· 9 years ago · Oct 09, 2016, 05:22 AM
1function Initialize()
2 tCurrentStreams = {}
3 isFirstLoad = true
4 skipNotif = false
5 isNotifying = false
6 gTotalOnline = 0
7 TokenErrorCnt = 0
8 DataErrorCnt = 0
9 path = SKIN:GetVariable('CURRENTPATH')
10 resourcePath = SKIN:GetVariable('@')
11 getState()
12
13 JSON = JSONDecode()
14 -- Handle my own errors with the JSON module, so just return
15 function JSON:assert(message, text, location, etc)
16 return
17 end
18end
19
20function getState()
21 -- SortArrow
22 local sortType = SKIN:GetVariable('sortType', 'ViewersDESC')
23 if (sortType == "ViewersDESC") then
24 SKIN:Bang('!SetOption', 'MeterSortArrow', 'X', '(#skinWidth# - 82)')
25 SKIN:Bang('!SetOption', 'MeterSortArrow', 'ImageFlip', 'None')
26 elseif (sortType == "ViewersASC") then
27 SKIN:Bang('!SetOption', 'MeterSortArrow', 'ImageFlip', 'Vertical')
28 SKIN:Bang('!SetOption', 'MeterSortArrow', 'X', '(#skinWidth# - 82)')
29 elseif (sortType == "NameDESC") then
30 SKIN:Bang('!SetOption', 'MeterSortArrow', 'X', '5R')
31 SKIN:Bang('!SetOption', 'MeterSortArrow', 'ImageFlip', 'None')
32 elseif (sortType == "NameASC") then
33 SKIN:Bang('!SetOption', 'MeterSortArrow', 'X', '5R')
34 SKIN:Bang('!SetOption', 'MeterSortArrow', 'ImageFlip', 'Vertical')
35 end
36
37 -- Collapsed/Expanded
38
39 local Collapsed = tonumber( SKIN:GetVariable('Collapsed') )
40 if (Collapsed == 0) then
41 SKIN:Bang('!SetOption', 'MeterExpCol', 'ButtonImage', '#@#Images\\Twitch\\Collapse.png')
42 SKIN:Bang('!SetOption', 'MeterExpCol', 'ToolTipTitle', 'Hide Live Channels')
43 SKIN:Bang('!SetOption', 'MeterExpCol', 'ToolTipText', 'Collapse skin to hide all live channels (Online notifications will still appear)')
44 else
45 SKIN:Bang('!SetOption', 'MeterExpCol', 'ButtonImage', '#@#Images\\Twitch\\Expand.png')
46 SKIN:Bang('!SetOption', 'MeterExpCol', 'ToolTipTitle', 'Show Live Channels')
47 SKIN:Bang('!SetOption', 'MeterExpCol', 'ToolTipText', 'Expand skin to show all live channels')
48 end
49
50 -- VLC ToolTip Quality
51 local quality = SKIN:GetVariable('Quality')
52 if (quality == "Source") then
53 SKIN:Bang('[!SetOption MeterVLCIcon ToolTipTitle "Watch Using Livestreamer (Current Quality: Source)"]')
54 elseif (quality == "High") then
55 SKIN:Bang('[!SetOption MeterVLCIcon ToolTipTitle "Watch Using Livestreamer (Current Quality: High)"]')
56 elseif (quality == "Medium") then
57 SKIN:Bang('[!SetOption MeterVLCIcon ToolTipTitle "Watch Using Livestreamer (Current Quality: Medium)"]')
58 elseif (quality == "Low") then
59 SKIN:Bang('[!SetOption MeterVLCIcon ToolTipTitle "Watch Using Livestreamer (Current Quality: Low)"]')
60 elseif (quality == "Mobile") then
61 SKIN:Bang('[!SetOption MeterVLCIcon ToolTipTitle "Watch Using Livestreamer (Current Quality: Mobile)"]')
62 end
63
64 SKIN:Bang('!UpdateMeter', '*')
65 SKIN:Bang('!Redraw')
66end
67
68function detectLivestreamer()
69 local MeasureOBJ = SKIN:GetMeasure('MeasureDetectLivestreamer')
70 local PATH = ( MeasureOBJ:GetStringValue() )
71
72 if ( string.find(PATH, "Livestreamer") or string.find(PATH, "livestreamer") ) then
73 SKIN:Bang('!SetVariable', 'Livestreamer', '1')
74 else
75 SKIN:Bang('!SetVariable', 'Livestreamer', '0')
76 end
77 SKIN:Bang('[!DisableMeasure MeasureCheckForLivestreamer]')
78end
79
80function getError(errorType)
81 if (errorType == 401) then
82 SKIN:Bang('!SetOption', 'MeterErrorHelp', 'ToolTipTitle', 'Access Token Error')
83 SKIN:Bang('!SetOption', 'MeterErrorHelp', 'LeftMouseUpAction', 'http://wallboy.ca/rainmeter/faq/#accesstoken')
84 SKIN:Bang('!SetOption', 'MeterErrorDesc', 'Text', 'Access Token Error')
85 SKIN:Bang('!SetOption', 'MeterErrorDesc', 'LeftMouseUpAction', 'http://wallboy.ca/rainmeter/faq/#accesstoken')
86 elseif (errorType == 500) then
87 SKIN:Bang('!SetOption', 'MeterErrorHelp', 'ToolTipTitle', 'Error Fetching Data')
88 SKIN:Bang('!SetOption', 'MeterErrorHelp', 'LeftMouseUpAction', 'http://wallboy.ca/rainmeter/faq/#dataerror')
89 SKIN:Bang('!SetOption', 'MeterErrorDesc', 'Text', 'Error Fetching Data')
90 SKIN:Bang('!SetOption', 'MeterErrorDesc', 'LeftMouseUpAction', 'http://wallboy.ca/rainmeter/faq/#dataerror')
91 end
92
93 SKIN:Bang('!ShowMeterGroup', 'ErrorBar')
94 SKIN:Bang('[!UpdateMeter *][!Redraw]')
95end
96
97function validateData(tJSON)
98 -- Checks for access token and data errors when the skin is first refreshed/loaded
99 if isFirstLoad then
100 if not tJSON then
101 getError(500)
102 return false
103 else
104 if (tJSON.status == 401) then
105 getError(401)
106 return false
107 else
108 SKIN:Bang('!HideMeterGroup', 'ErrorBar')
109 end
110 end
111 else
112 -- If tJSON returns HTML such as a 503 or 502 error, we skip the error for up to 5 updates. If the error persists, we notify the user.
113 if not tJSON then
114 DataErrorCnt = DataErrorCnt + 1
115 if (DataErrorCnt == 5) then
116 gTotalOnline = 0
117 isFirstLoad = true
118 printTable()
119 getError(500)
120 return false
121 end
122 return false
123 end
124 -- Same as above, except this checks for "false positive" access token errors. Only after 5 failed updates do we notify the user.
125 if (tJSON.status == 401) then
126 TokenErrorCnt = TokenErrorCnt + 1
127 if (TokenErrorCnt == 5) then
128 gTotalOnline = 0
129 isFirstLoad = true
130 printTable()
131 getError(401)
132 return false
133 end
134 return false
135 end
136 end
137
138 -- Quick "hack". Twitch API can drop to 0 online for a single update, creating a possible long list of online notif's next update, so we skip an update when this happens.
139 if ( (tJSON._total == 0) and (gTotalOnline >= 1) ) then
140 gTotalOnline = 0
141 return false
142 end
143
144 -- If we made it to here, no errors
145 TokenErrorCnt = 0
146 DataErrorCnt = 0
147 return true
148end
149
150function getProfileThumbs()
151 while ( NotifIndex <= LogosToProcess ) do
152 local profileThumbKey
153
154 if (tNewStream[NotifIndex].LogoURL ~= nil) then
155 profileThumbKey = string.match(tNewStream[NotifIndex].LogoURL, 'image%-(.+)-')
156 else
157 profileThumbKey = "NoProfileImage"
158 end
159
160 if ( profileThumbKeys(tNewStream[NotifIndex].name, profileThumbKey) ) then
161 return
162 end
163 end
164 fadeNotif(1)
165end
166
167function downloadLogo()
168
169 if not tNewStream[NotifIndex].LogoURL then
170 SKIN:Bang('!SetOption', 'MeasureProfThumb', 'URL', 'http://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png')
171 else
172 SKIN:Bang('!SetOption', 'MeasureProfThumb', 'URL', tNewStream[NotifIndex].LogoURL)
173 end
174 SKIN:Bang('!Setoption', 'MeasureProfThumb', 'DownloadFile', 'ProfileThumbs\\'..tNewStream[NotifIndex].name..'.png')
175
176 if (NotifIndex == LogosToProcess) then
177 SKIN:Bang('!SetOption', 'MeasureResize', 'FinishAction', '[!CommandMeasure MeasureLUA "fadeNotif(1)"]')
178 end
179
180 SKIN:Bang('!SetOption', 'MeasureResize', 'Parameter', '-overwrite -quiet -out png -resize 40 40 "'..path..'DownloadFile\\ProfileThumbs\\'..tNewStream[NotifIndex].name..'.png"')
181
182 NotifIndex = NotifIndex + 1
183
184 SKIN:Bang('[!UpdateMeasure MeasureResize]')
185
186 SKIN:Bang('[!EnableMeasure MeasureProfThumb]')
187 SKIN:Bang('[!CommandMeasure MeasureProfThumb Update]')
188 SKIN:Bang('[!UpdateMeasure MeasureProfThumb]')
189end
190
191function profileThumbKeys(streamName, profileKey)
192 local hFile = io.open(resourcePath..'profileKeys.txt', 'r')
193 if (hFile == nil) then
194 hFile = io.open(resourcePath..'profileKeys.txt', 'w')
195 hFile:close()
196 hFile = assert(io.open(resourcePath..'profileKeys.txt', 'r'), 'Unable to open profileKeys.txt')
197 end
198
199 local lines = {}
200 local restOfFile = ""
201 local foundLine = false
202 local needDownload = false
203 local needWrite = false
204
205 local f = io.open(path..'DownloadFile\\ProfileThumbs\\'..tNewStream[NotifIndex].name..'.png', "r")
206 if (f == nil) then
207 needDownload = true
208 else
209 io.close(f)
210 end
211
212 for line in hFile:lines() do
213 local lineStreamName, lineProfileKey = line:match('^([^=]+)=(.+)')
214 if(streamName == lineStreamName) then
215 foundLine = true
216 if (profileKey ~= lineProfileKey) then
217 lines[#lines + 1] = streamName.."="..profileKey
218 restOfFile = hFile:read("*a")
219 needDownload = true
220 needWrite = true
221 break
222 else
223 lines[#lines + 1] = line
224 end
225 else
226 lines[#lines + 1] = line
227 end
228 end
229
230 hFile:close()
231
232 if (foundLine == false) then
233 lines[#lines + 1] = streamName.."="..profileKey
234 needDownload = true
235 needWrite = true
236 end
237
238 if needWrite then
239 hFile = assert(io.open(resourcePath..'profileKeys.txt', 'w'), 'Unable to open profileKeys.txt')
240 for i, line in ipairs(lines) do
241 hFile:write(line, "\n")
242 end
243 hFile:write(restOfFile)
244 hFile:close()
245 end
246
247 if needDownload then
248 downloadLogo()
249 return true
250 else
251 NotifIndex = NotifIndex + 1
252 end
253end
254
255function fadeNotif(setup)
256 if ( tonumber(SKIN:GetVariable('Notifications')) == 0 or isFirstLoad ) then -- Don't show them on first skin load/refresh
257 isFirstLoad = false
258 return
259 end
260
261 if setup then
262 SKIN:Bang('!SetOption', 'MeterNotifName', 'FontColor', '#*colorNotifName*#')
263 SKIN:Bang('!SetOption', 'MeterNotifBorder', 'ImageTint', '#*colorOnlineBorder*#')
264 SKIN:Bang('!SetOption', 'MeterNotifSubtitle', 'FontColor', '#*colorComeOnline*#')
265 SKIN:Bang('!SetOption', 'MeterNotifSubtitle', 'Text', 'has come online')
266 SKIN:Bang('!SetOptionGroup', 'Notif', 'Hidden', '0')
267
268 SKIN:Bang('!HideMeter', 'MeterGlitch')
269
270 NotifIndex = 1
271 isNotifying = true -- lock notif area in use
272
273 SKIN:Bang('[!EnableMeasure MeasureNotifTimer]')
274 end
275
276 if (NotifIndex <= LogosToProcess) then
277 SKIN:Bang('!SetOption', 'MeterNotifName', 'Text', tNewStream[NotifIndex].display_name)
278 SKIN:Bang('!SetOption', 'MeterNotifName', 'LeftMouseUpAction', tNewStream[NotifIndex].URL)
279 SKIN:Bang('!SetOption', 'MeterNotifThumb', 'ImageName', path..'DownloadFile\\ProfileThumbs\\'..tNewStream[NotifIndex].name..'.png')
280 SKIN:Bang('!SetOption', 'MeterNotifThumb', 'LeftMouseUpAction', tNewStream[NotifIndex].URL)
281 SKIN:Bang('[!UpdateMeter *][!Redraw]')
282
283 NotifIndex = NotifIndex + 1
284 return
285
286 else
287 SKIN:Bang('!DisableMeasure', 'MeasureNotifTimer')
288 SKIN:Bang('!SetOption', 'MeterNotifName', 'LeftMouseUpAction', '')
289 SKIN:Bang('!SetOption', 'MeterNotifThumb', 'LeftMouseUpAction', '')
290 SKIN:Bang('!SetOptionGroup', 'Notif', 'Hidden', '1')
291 SKIN:Bang('!ShowMeter', 'MeterGlitch')
292
293 SKIN:Bang('[!UpdateMeter *][!Redraw]')
294 isNotifying = false -- unlock notif area
295 end
296end
297
298function changeSort(Side)
299 local sortType = SKIN:GetVariable('sortType', 'ViewersDESC')
300 -- Sort by ViewersASC from ViewersDESC
301 if (sortType == "ViewersDESC") and (Side == "R") then
302 SKIN:Bang('!SetOption', 'MeterSortArrow', 'ImageFlip', 'Vertical')
303 SKIN:Bang('!SetVariable', 'sortType', 'ViewersASC')
304 SKIN:Bang('[!WriteKeyValue "Variables" "sortType" "ViewersASC" "#@#Variables.inc"]')
305 -- Sort by NameDESC from ViewersDESC
306 elseif (sortType == "ViewersDESC") and (Side == "L") then
307 SKIN:Bang('!SetOption', 'MeterSortArrow', 'X', '5R')
308 SKIN:Bang('!SetVariable', 'sortType', 'NameDESC')
309 SKIN:Bang('[!WriteKeyValue "Variables" "sortType" "NameDESC" "#@#Variables.inc"]')
310 -- Sort by ViewersDESC from ViewersASC
311 elseif (sortType == "ViewersASC") and (Side == "R") then
312 SKIN:Bang('!SetOption', 'MeterSortArrow', 'ImageFlip', 'None')
313 SKIN:Bang('!SetVariable', 'sortType', 'ViewersDESC')
314 SKIN:Bang('[!WriteKeyValue "Variables" "sortType" "ViewersDESC" "#@#Variables.inc"]')
315 -- Sort by NameASC from ViewersASC
316 elseif (sortType == "ViewersASC") and (Side == "L") then
317 SKIN:Bang('!SetOption', 'MeterSortArrow', 'X', '5R')
318 SKIN:Bang('!SetVariable', 'sortType', 'NameASC')
319 SKIN:Bang('[!WriteKeyValue "Variables" "sortType" "NameASC" "#@#Variables.inc"]')
320 -- Sort by ViewersDESC from NameDESC
321 elseif (sortType == "NameDESC") and (Side == "R") then
322 SKIN:Bang('!SetOption', 'MeterSortArrow', 'X', '(#skinWidth# - 82)')
323 SKIN:Bang('!SetVariable', 'sortType', 'ViewersDESC')
324 SKIN:Bang('[!WriteKeyValue "Variables" "sortType" "ViewersDESC" "#@#Variables.inc"]')
325 -- Sort by NameASC from NameDESC
326 elseif (sortType == "NameDESC") and (Side == "L") then
327 SKIN:Bang('!SetOption', 'MeterSortArrow', 'ImageFlip', 'Vertical')
328 SKIN:Bang('!SetVariable', 'sortType', 'NameASC')
329 SKIN:Bang('[!WriteKeyValue "Variables" "sortType" "NameASC" "#@#Variables.inc"]')
330 -- Sort by ViewersASC from NameASC
331 elseif (sortType == "NameASC") and (Side == "R") then
332 SKIN:Bang('!SetOption', 'MeterSortArrow', 'X', '(#skinWidth# - 82)')
333 SKIN:Bang('!SetVariable', 'sortType', 'ViewersASC')
334 SKIN:Bang('[!WriteKeyValue "Variables" "sortType" "ViewersASC" "#@#Variables.inc"]')
335 -- Sort by NameDESC from NameASC
336 elseif (sortType == "NameASC") and (Side == "L") then
337 SKIN:Bang('!SetOption', 'MeterSortArrow', 'ImageFlip', 'None')
338 SKIN:Bang('!SetVariable', 'sortType', 'NameDESC')
339 SKIN:Bang('[!WriteKeyValue "Variables" "sortType" "NameDESC" "#@#Variables.inc"]')
340 end
341
342 sortTable()
343 printTable()
344 SKIN:Bang('!UpdateMeter', '*')
345 SKIN:Bang('!Redraw')
346end
347
348function collapseExpand()
349 local Collapsed = tonumber( SKIN:GetVariable('Collapsed') )
350
351 -- Collapse Skin
352 if (Collapsed == 0) then
353 SKIN:Bang('!SetVariable', 'Collapsed', '1')
354 SKIN:Bang('!WriteKeyValue', 'Variables', 'Collapsed', '1', '#@#Variables.inc')
355
356 SKIN:Bang('!SetOption', 'MeterExpCol', 'ButtonImage', '#@#Images\\Twitch\\Expand.png')
357
358 SKIN:Bang('!SetOption', 'MeterExpCol', 'ToolTipTitle', 'Show Live Channels')
359 SKIN:Bang('!SetOption', 'MeterExpCol', 'ToolTipText', 'Expand skin to show all live channels')
360
361 SKIN:Bang('!SetOptionGroup', 'DropDown', 'Hidden', '1')
362
363 SKIN:Bang('!SetOption', 'MeterBottomCurve', 'ImageTint', '#colorTitleBar2#')
364
365 -- Expand Skin
366 else
367 SKIN:Bang('!SetVariable', 'Collapsed', '0')
368 SKIN:Bang('!WriteKeyValue', 'Variables', 'Collapsed', '0', '#@#Variables.inc')
369
370 SKIN:Bang('!SetOption', 'MeterExpCol', 'ButtonImage', '#@#Images\\Twitch\\Collapse.png')
371
372 SKIN:Bang('!SetOption', 'MeterExpCol', 'ToolTipTitle', 'Hide Live Channels')
373 SKIN:Bang('!SetOption', 'MeterExpCol', 'ToolTipText', 'Collapse skin to hide all live channels (Online notifications will still appear)')
374
375 SKIN:Bang('!SetOptionGroup', 'DropDown', 'Hidden', '')
376 SKIN:Bang('!SetOptionGroup', 'DropDown', 'DynamicVariables', '0')
377
378 if ( (gTotalOnline % 2) == 0 ) then
379 SKIN:Bang('!SetOption', 'MeterBottomCurve', 'ImageTint', '#colorChannelBarAlt2#')
380 else
381 SKIN:Bang('!SetOption', 'MeterBottomCurve', 'ImageTint', '#colorChannelBar2#')
382 end
383 end
384
385 SKIN:Bang('!SetOption', 'MeterSideBorder', 'DynamicVariables', '0')
386
387 SKIN:Bang('[!UpdateMeter *][!Redraw]')
388end
389
390function updateValues()
391 local mainFeedOBJ = SKIN:GetMeasure("MeasureJSONFeed")
392 local rawJSON = mainFeedOBJ:GetStringValue()
393 local tJSON = JSON:decode(rawJSON)
394
395 -- Validates access token, and checks for any errors in the JSON decode
396 if not validateData(tJSON) then
397 return
398 end
399
400 gTotalOnline = tJSON._total
401
402 -- Cap to maximum supported meters
403 if (gTotalOnline > 30) then
404 gTotalOnline = 30
405 end
406
407 tNewStream = {}
408 local tTemp = {}
409 local ThumbsDisabled = tonumber( SKIN:GetVariable('ThumbsDisabled') )
410 if (ThumbsDisabled ~= 1) then
411 gThumbUpdTime = 5
412 else
413 gThumbUpdTime = 0
414 end
415
416 for stream, field in ipairs(tJSON.streams) do
417 local found = false
418 for k, v in ipairs(tCurrentStreams) do
419 if (v.name == field.channel.name) then
420 tTemp[#tTemp + 1] = v
421
422 tTemp[#tTemp].game = field.game
423 tTemp[#tTemp].viewers = field.viewers
424 if (ThumbsDisabled ~= 1) then
425 tTemp[#tTemp].ThumbUpdTime = v.ThumbUpdTime + 1
426 else
427 tTemp[#tTemp].ThumbUpdTime = 0
428 end
429 found = true
430 break
431 end
432 end
433
434 if not found then
435 tNewStream[#tNewStream + 1] = {
436 ["name"] = field.channel.name,
437 ["display_name"] = field.channel.display_name,
438 ["game"] = field.game,
439 ["URL"] = "http://www.twitch.tv/"..field.channel.name, -- field.channel.URL is sometimes blank. So we concatenate instead.
440 ["Chat"] ="https://www.twitch.tv/"..field.channel.name.."/chat?popout=", --code Firecyclones edited
441 ["viewers"] = field.viewers,
442 ["ThumbURL"] = field.preview.template,
443 ["LogoURL"] = field.channel.logo,
444 ["ThumbUpdTime"] = gThumbUpdTime
445 }
446 tTemp[#tTemp + 1] = tNewStream[#tNewStream]
447 end
448 end
449
450 tCurrentStreams = {}
451 tCurrentStreams = tTemp
452
453 ---- SORT/PRINT------
454
455 sortTable()
456 printTable()
457
458 --- ONLINE NOTIFS ---
459
460 if ( next(tNewStream) ~= nil ) then
461 LogosToProcess = #tNewStream
462 NotifIndex = 1
463
464 SKIN:Bang('!SetOption', 'MeasureResize', 'FinishAction', '[!CommandMeasure MeasureLUA "getProfileThumbs()"]')
465
466 getProfileThumbs()
467 end
468
469 if (isFirstLoad and gTotalOnline == 0) then -- Quick hack. isFirstLoad = false gets skipped if the skin is initially loaded with 0 channels online.
470 isFirstLoad = false
471 end
472end
473
474function showThumbnail(meter)
475 -- MouseLeaveAction:
476 if not meter then
477 local ThumbsDisabled = tonumber( SKIN:GetVariable('ThumbsDisabled') )
478 if (ThumbsDisabled ~= 1) then
479 SKIN:Bang('[!DeactivateConfig "TwitchLiveFollowers\\Thumbnail"]')
480 end
481 return
482 end
483
484 local Thumbnails = tonumber( SKIN:GetVariable('Thumbnails') )
485
486 -- Display Thumbnail on left or right depending on screen position
487 if ( Thumbnails < 2 ) then -- 0 = Right, 1 = Left, >=2 = Off
488 local LastConfigX = tonumber( SKIN:GetVariable('LastConfigX') )
489 local CURRENTCONFIGX = tonumber( SKIN:GetVariable('CURRENTCONFIGX') )
490
491 -- Only calculate if the skin was moved
492 if ( CURRENTCONFIGX ~= LastConfigX ) then
493 local CURRENTCONFIGY = tonumber(SKIN:GetVariable('CURRENTCONFIGY'))
494
495 local WORKAREAX = tonumber(SKIN:GetVariable('WORKAREAX'))
496 local WORKAREAY = tonumber(SKIN:GetVariable('WORKAREAY'))
497
498 local WORKAREAWIDTH = tonumber(SKIN:GetVariable('WORKAREAWIDTH'))
499 local WORKAREAHEIGHT = tonumber(SKIN:GetVariable('WORKAREAHEIGHT'))
500
501 local CURRENTCONFIGWIDTH = tonumber(SKIN:GetVariable('skinWidth'))
502 local CURRENTCONFIGHEIGHT = tonumber(SKIN:GetVariable('CURRENTCONFIGHEIGHT'))
503
504 if ( (CURRENTCONFIGX < 200 + WORKAREAX) and (CURRENTCONFIGX > WORKAREAX - 200) ) then
505 SKIN:Bang('!SetVariable', 'Thumbnails', '0')
506 SKIN:Bang('!WriteKeyValue', 'Variables', 'Thumbnails', '0', '#@#Variables.inc')
507
508 elseif ( (CURRENTCONFIGX < 200 + ((WORKAREAWIDTH + WORKAREAX) - CURRENTCONFIGWIDTH)) and (CURRENTCONFIGX > -200 + ((WORKAREAWIDTH + WORKAREAX) - CURRENTCONFIGWIDTH)) ) then
509 SKIN:Bang('!SetVariable', 'Thumbnails', '1')
510 SKIN:Bang('!WriteKeyValue', 'Variables', 'Thumbnails', '1', '#@#Variables.inc')
511
512 else
513 SKIN:Bang('!SetVariable', 'Thumbnails', '1')
514 SKIN:Bang('!WriteKeyValue', 'Variables', 'Thumbnails', '1', '#@#Variables.inc')
515 end
516 SKIN:Bang('!SetVariable', 'LastConfigX', CURRENTCONFIGX)
517 SKIN:Bang('!WriteKeyValue', 'Variables', 'LastCOnfigX', CURRENTCONFIGX, '#@#Variables.inc')
518 end
519
520 if ( tonumber(SKIN:GetVariable('Thumbnails')) == 1 ) then
521 SKIN:Bang('!WriteKeyValue', 'TwitchLiveFollowers\\Thumbnail', 'WindowX', '(#CURRENTCONFIGX# - #skinWidth#)', '#SETTINGSPATH#Rainmeter.ini')
522 else
523 SKIN:Bang('!WriteKeyValue', 'TwitchLiveFollowers\\Thumbnail', 'WindowX', '(#CURRENTCONFIGX# + #skinWidth#)', '#SETTINGSPATH#Rainmeter.ini')
524 end
525 SKIN:Bang('!WriteKeyValue', 'TwitchLiveFollowers\\Thumbnail', 'WindowY', '(#CURRENTCONFIGY# -46 + #subtitleBarHeight# + #topBarHeight# + #meterHeight# *'..(meter - 1)..')', '#SETTINGSPATH#Rainmeter.ini')
526 -- Thumbnail Off
527 else
528 return -- Do nothing, thumbsnails are off
529 end
530 SKIN:Bang('!WriteKeyValue', 'MeterThumbnail', 'ImageName', '#*ROOTCONFIGPATH*#Twitch\\DownloadFile\\StreamThumbs\\'..tCurrentStreams[meter].name..'.png', '#ROOTCONFIGPATH#Thumbnail\\thumb.ini')
531
532 SKIN:Bang('[!ActivateConfig TwitchLiveFollowers\\Thumbnail]')
533end
534
535function streamHover(i, leave)
536 -- MouseLeaveAction
537 local meterOnline = SKIN:GetMeter('MeterNumOnline')
538
539 if leave then
540 if (isNotifying == false) then -- Only hide it if no online notifications are currently running.
541 SKIN:Bang('[!SetOptionGroup Notif Hidden 1]')
542 SKIN:Bang('!ShowMeter', 'MeterGlitch')
543 end
544 SKIN:Bang('[!HideMeter MeterVLCIcon]')
545 SKIN:Bang('[!HideMeter MeterChatIcon]')
546
547 SKIN:Bang('!SetOption', 'MeterChanBar'..i, 'SolidColor', '')
548 SKIN:Bang('!SetOption', 'MeterChanBar'..i, 'SolidColor2', '')
549
550 if (i == gTotalOnline) then
551 if ( (i % 2) ~= 0) then
552 SKIN:Bang('!SetOption', 'MeterBottomCurve', 'ImageTint', '#*colorChannelBar2*#')
553 else
554 SKIN:Bang('!SetOption', 'MeterBottomCurve', 'ImageTint', '#*colorChannelBarAlt2*#')
555 end
556 end
557
558 SKIN:Bang('!UpdateMeter', '*')
559 SKIN:Bang('!Redraw')
560 return
561 end
562
563 if ( tonumber(SKIN:GetVariable('Livestreamer')) == 1) then
564 local quality = SKIN:GetVariable('Quality')
565 SKIN:Bang('!SetOption', 'MeterVLCIcon', 'LeftMouseUpAction', '["livestreamer.exe" '..tCurrentStreams[i].URL..' '..quality..']')
566 SKIN:Bang('!SetOption', 'MeterVLCIcon', 'X', '(([MeterStreamName'..i..':X] + [MeterStreamName'..i..':W]) + 5)')
567 SKIN:Bang('!SetOption', 'MeterVLCIcon', 'Y', '([MeterStreamName'..i..':Y] + 1)')
568 SKIN:Bang('!ShowMeter', 'MeterVLCIcon')
569
570 --code Firecyclones edited
571 SKIN:Bang('!SetOption', 'MeterChatIcon', 'LeftMouseUpAction', '["'..tCurrentStreams[i].Chat..'"]')
572 SKIN:Bang('!SetOption', 'MeterChatIcon', 'X', '(([MeterStreamName'..i..':X] + [MeterStreamName'..i..':W]) + 20)')
573 SKIN:Bang('!SetOption', 'MeterChatIcon', 'Y', '([MeterStreamName'..i..':Y] + 1)')
574 SKIN:Bang('!ShowMeter', 'MeterChatIcon')
575 --code Firecyclones edited
576
577 end
578
579 if ( isNotifying == false ) then -- Only show if no online notifications are running
580 SKIN:Bang('!SetOption', 'MeterNotifThumb', 'ImageName', path..'DownloadFile\\ProfileThumbs\\'..tCurrentStreams[i].name..'.png')
581 SKIN:Bang('!SetOption', 'MeterNotifThumb', 'ImageAlpha', '255')
582 SKIN:Bang('!SetOption', 'MeterNotifBorder', 'ImageAlpha', '255')
583 SKIN:Bang('!SetOption', 'MeterNotifBorder', 'ImageTint', '')
584 SKIN:Bang('!SetOption', 'MeterNotifName', 'Text', 'Playing')
585 SKIN:Bang('!SetOption', 'MeterNotifName', 'FontColor', '#*colorPlayingText*#')
586 SKIN:Bang('!SetOption', 'MeterNotifSubtitle', 'Text', tCurrentStreams[i].game)
587 SKIN:Bang('!SetOption', 'MeterNotifSubtitle', 'FontColor', '#*colorGameName*#')
588
589 SKIN:Bang('!SetOptionGroup', 'Notif', 'Hidden', '0')
590 SKIN:Bang('!HideMeter', 'MeterGlitch')
591 end
592
593 SKIN:Bang('!SetOption', 'MeterChanBar'..i, 'SolidColor', '#colorChannelBarHover#')
594 SKIN:Bang('!SetOption', 'MeterChanBar'..i, 'SolidColor2', '#colorChannelBarHover2#')
595
596 if (i == gTotalOnline) then
597 SKIN:Bang('!SetOption', 'MeterBottomCurve', 'ImageTint', '#colorChannelBarHover2#')
598 end
599
600 SKIN:Bang('!UpdateMeter', '*')
601 SKIN:Bang('!Redraw')
602end
603
604function sortTable()
605 local sortType = SKIN:GetVariable('sortType', 'ViewersDESC')
606
607 if (sortType == "ViewersDESC") then
608 table.sort(tCurrentStreams, function (a,b) return a.viewers > b.viewers end)
609 elseif (sortType == "ViewersASC") then
610 table.sort(tCurrentStreams, function (a,b) return a.viewers < b.viewers end)
611 elseif (sortType == "NameDESC") then
612 table.sort(tCurrentStreams, function (a,b) return a.name > b.name end)
613 elseif (sortType == "NameASC") then
614 table.sort(tCurrentStreams, function (a,b) return a.name < b.name end)
615 end
616
617end
618
619function printTable()
620 for k, v in ipairs(tCurrentStreams) do
621 if (v.ThumbUpdTime == 5) then -- Thumbnail older than 5 minutes? Update.
622 SKIN:Bang('!SetOption', 'MeasureIMG'..k, 'URL', customThumbnailSize( v.ThumbURL ))
623 SKIN:Bang('!SetOption', 'MeasureIMG'..k, 'DownloadFile', 'StreamThumbs\\'..v.name..'.png')
624 SKIN:Bang('!EnableMeasure', 'MeasureIMG'..k)
625 SKIN:Bang('!CommandMeasure', 'MeasureIMG'..k, 'Update')
626 SKIN:Bang('!UpdateMeasure', 'MeasureIMG'..k)
627 v.ThumbUpdTime = 0
628 end
629
630 SKIN:Bang('!SetOption', 'MeterStreamName'..k, 'Text', v.display_name)
631 SKIN:Bang('!SetOption', 'MeterChanBar'..k, 'LeftMouseUpAction', '["'..v.URL..'"]')
632 SKIN:Bang('!SetOption', 'MeterViewers'..k, 'Text', formatViewers(v.viewers))
633 end
634
635 if (gTotalOnline <= 29) then
636 SKIN:Bang('!SetOption', 'MeterNumOnline', 'Text', gTotalOnline)
637 else
638 SKIN:Bang('!SetOption', 'MeterNumOnline', 'Text', '30+')
639 end
640
641 if ( (tonumber( SKIN:GetVariable('Collapsed') )) == 0) then
642 if ( (gTotalOnline % 2) == 0 ) then
643 SKIN:Bang('!SetOption', 'MeterBottomCurve', 'ImageTint', '#*colorChannelBarAlt2*#')
644 else
645 SKIN:Bang('!SetOption', 'MeterBottomCurve', 'ImageTint', '#*colorChannelBar2*#')
646 end
647 end
648
649 SKIN:Bang('!SetVariable', 'numOnline', gTotalOnline)
650 SKIN:Bang('!SetOption', 'MeterSideBorder', 'DynamicVariables', '0')
651
652 if ( tonumber(SKIN:GetVariable('Collapsed')) == 0 ) then
653 SKIN:Bang('!SetOptionGroup', 'DropDown', 'DynamicVariables', '0')
654 end
655
656 SKIN:Bang('[!UpdateMeter *][!Redraw]')
657end
658
659function formatViewers(numVar)
660 -- Modified function from: http://docs.rainmeter.net/snippets/add-commas
661 if ( tonumber(SKIN:GetVariable('numFormat')) ) == 0 then -- Full version with commas
662 assert(tonumber(numVar), 'Function formatViewers() expects a number.')
663 local prefix, number, postfix = string.match(numVar, '^([^%d]*%d)(%d*)(.-)$')
664
665 return prefix..(number:reverse():gsub('(%d%d%d)', '%1,'):reverse())..postfix
666 else -- Short version with rounding
667 if numVar >= 10^6 then
668 return string.format("%.2f", numVar / 10^6) .. "m"
669 elseif numVar >= 10^3 then
670 return string.format("%.1f", numVar / 10^3) .. "k"
671 else
672 return numVar
673 end
674 end
675end
676
677function customThumbnailSize(urlStr)
678 local s = string.gsub(urlStr, "{width}", "184")
679 return (string.gsub(s, "{height}", "104"))
680end
681
682function JSONDecode()
683---------------------------------------------------------------------------------------------------------------------------------------------
684-- START OF JSON SCRIPT ---------------------
685---------------------------------------------------------------------------------------------------------------------------------------------
686
687
688
689-- -*- coding: utf-8 -*-
690--
691-- Simple JSON encoding and decoding in pure Lua.
692--
693-- Copyright 2010-2013 Jeffrey Friedl
694-- http://regex.info/blog/
695--
696-- Latest version: http://regex.info/blog/lua/json
697--
698-- This code is released under a Creative Commons CC-BY "Attribution" License:
699-- http://creativecommons.org/licenses/by/3.0/deed.en_US
700--
701-- It can be used for any purpose so long as the copyright notice and
702-- web-page links above are maintained. Enjoy.
703--
704local VERSION = 20140418.11 -- version history at end of file
705local OBJDEF = { VERSION = VERSION }
706
707
708--
709-- Simple JSON encoding and decoding in pure Lua.
710-- http://www.json.org/
711--
712--
713-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines
714--
715-- local lua_value = JSON:decode(raw_json_text)
716--
717-- local raw_json_text = JSON:encode(lua_table_or_value)
718-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability
719--
720--
721-- DECODING
722--
723-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines
724--
725-- local lua_value = JSON:decode(raw_json_text)
726--
727-- If the JSON text is for an object or an array, e.g.
728-- { "what": "books", "count": 3 }
729-- or
730-- [ "Larry", "Curly", "Moe" ]
731--
732-- the result is a Lua table, e.g.
733-- { what = "books", count = 3 }
734-- or
735-- { "Larry", "Curly", "Moe" }
736--
737--
738-- The encode and decode routines accept an optional second argument, "etc", which is not used
739-- during encoding or decoding, but upon error is passed along to error handlers. It can be of any
740-- type (including nil).
741--
742-- With most errors during decoding, this code calls
743--
744-- JSON:onDecodeError(message, text, location, etc)
745--
746-- with a message about the error, and if known, the JSON text being parsed and the byte count
747-- where the problem was discovered. You can replace the default JSON:onDecodeError() with your
748-- own function.
749--
750-- The default onDecodeError() merely augments the message with data about the text and the
751-- location if known (and if a second 'etc' argument had been provided to decode(), its value is
752-- tacked onto the message as well), and then calls JSON.assert(), which itself defaults to Lua's
753-- built-in assert(), and can also be overridden.
754--
755-- For example, in an Adobe Lightroom plugin, you might use something like
756--
757-- function JSON:onDecodeError(message, text, location, etc)
758-- LrErrors.throwUserError("Internal Error: invalid JSON data")
759-- end
760--
761-- or even just
762--
763-- function JSON.assert(message)
764-- LrErrors.throwUserError("Internal Error: " .. message)
765-- end
766--
767-- If JSON:decode() is passed a nil, this is called instead:
768--
769-- JSON:onDecodeOfNilError(message, nil, nil, etc)
770--
771-- and if JSON:decode() is passed HTML instead of JSON, this is called:
772--
773-- JSON:onDecodeOfHTMLError(message, text, nil, etc)
774--
775-- The use of the fourth 'etc' argument allows stronger coordination between decoding and error
776-- reporting, especially when you provide your own error-handling routines. Continuing with the
777-- the Adobe Lightroom plugin example:
778--
779-- function JSON:onDecodeError(message, text, location, etc)
780-- local note = "Internal Error: invalid JSON data"
781-- if type(etc) = 'table' and etc.photo then
782-- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName')
783-- end
784-- LrErrors.throwUserError(note)
785-- end
786--
787-- :
788-- :
789--
790-- for i, photo in ipairs(photosToProcess) do
791-- :
792-- :
793-- local data = JSON:decode(someJsonText, { photo = photo })
794-- :
795-- :
796-- end
797--
798--
799--
800--
801
802-- DECODING AND STRICT TYPES
803--
804-- Because both JSON objects and JSON arrays are converted to Lua tables, it's not normally
805-- possible to tell which a JSON type a particular Lua table was derived from, or guarantee
806-- decode-encode round-trip equivalency.
807--
808-- However, if you enable strictTypes, e.g.
809--
810-- JSON = (loadfile "JSON.lua")() --load the routines
811-- JSON.strictTypes = true
812--
813-- then the Lua table resulting from the decoding of a JSON object or JSON array is marked via Lua
814-- metatable, so that when re-encoded with JSON:encode() it ends up as the appropriate JSON type.
815--
816-- (This is not the default because other routines may not work well with tables that have a
817-- metatable set, for example, Lightroom API calls.)
818--
819--
820-- ENCODING
821--
822-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines
823--
824-- local raw_json_text = JSON:encode(lua_table_or_value)
825-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability
826
827-- On error during encoding, this code calls:
828--
829-- JSON:onEncodeError(message, etc)
830--
831-- which you can override in your local JSON object.
832--
833-- If the Lua table contains both string and numeric keys, it fits neither JSON's
834-- idea of an object, nor its idea of an array. To get around this, when any string
835-- key exists (or when non-positive numeric keys exist), numeric keys are converted
836-- to strings.
837--
838-- For example,
839-- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" }))
840-- produces the JSON object
841-- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"}
842--
843-- To prohibit this conversion and instead make it an error condition, set
844-- JSON.noKeyConversion = true
845
846
847--
848-- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT
849--
850-- assert
851-- onDecodeError
852-- onDecodeOfNilError
853-- onDecodeOfHTMLError
854-- onEncodeError
855--
856-- If you want to create a separate Lua JSON object with its own error handlers,
857-- you can reload JSON.lua or use the :new() method.
858--
859---------------------------------------------------------------------------
860
861
862local author = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json), version " .. tostring(VERSION) .. " ]-"
863local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray
864local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject
865
866
867function OBJDEF:newArray(tbl)
868 return setmetatable(tbl or {}, isArray)
869end
870
871function OBJDEF:newObject(tbl)
872 return setmetatable(tbl or {}, isObject)
873end
874
875local function unicode_codepoint_as_utf8(codepoint)
876 --
877 -- codepoint is a number
878 --
879 if codepoint <= 127 then
880 return string.char(codepoint)
881
882 elseif codepoint <= 2047 then
883 --
884 -- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8
885 --
886 local highpart = math.floor(codepoint / 0x40)
887 local lowpart = codepoint - (0x40 * highpart)
888 return string.char(0xC0 + highpart,
889 0x80 + lowpart)
890
891 elseif codepoint <= 65535 then
892 --
893 -- 1110yyyy 10yyyyxx 10xxxxxx
894 --
895 local highpart = math.floor(codepoint / 0x1000)
896 local remainder = codepoint - 0x1000 * highpart
897 local midpart = math.floor(remainder / 0x40)
898 local lowpart = remainder - 0x40 * midpart
899
900 highpart = 0xE0 + highpart
901 midpart = 0x80 + midpart
902 lowpart = 0x80 + lowpart
903
904 --
905 -- Check for an invalid character (thanks Andy R. at Adobe).
906 -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070
907 --
908 if ( highpart == 0xE0 and midpart < 0xA0 ) or
909 ( highpart == 0xED and midpart > 0x9F ) or
910 ( highpart == 0xF0 and midpart < 0x90 ) or
911 ( highpart == 0xF4 and midpart > 0x8F )
912 then
913 return "?"
914 else
915 return string.char(highpart,
916 midpart,
917 lowpart)
918 end
919
920 else
921 --
922 -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx
923 --
924 local highpart = math.floor(codepoint / 0x40000)
925 local remainder = codepoint - 0x40000 * highpart
926 local midA = math.floor(remainder / 0x1000)
927 remainder = remainder - 0x1000 * midA
928 local midB = math.floor(remainder / 0x40)
929 local lowpart = remainder - 0x40 * midB
930
931 return string.char(0xF0 + highpart,
932 0x80 + midA,
933 0x80 + midB,
934 0x80 + lowpart)
935 end
936end
937
938function OBJDEF:onDecodeError(message, text, location, etc)
939 if text then
940 if location then
941 message = string.format("%s at char %d of: %s", message, location, text)
942 else
943 message = string.format("%s: %s", message, text)
944 end
945 end
946
947 if etc ~= nil then
948 message = message .. " (" .. OBJDEF:encode(etc) .. ")"
949 end
950
951 if self.assert then
952 self.assert(false, message)
953 else
954 assert(false, message)
955 end
956end
957
958OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError
959OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError
960
961function OBJDEF:onEncodeError(message, etc)
962 if etc ~= nil then
963 message = message .. " (" .. OBJDEF:encode(etc) .. ")"
964 end
965
966 if self.assert then
967 self.assert(false, message)
968 else
969 assert(false, message)
970 end
971end
972
973local function grok_number(self, text, start, etc)
974 --
975 -- Grab the integer part
976 --
977 local integer_part = text:match('^-?[1-9]%d*', start)
978 or text:match("^-?0", start)
979
980 if not integer_part then
981 self:onDecodeError("expected number", text, start, etc)
982 end
983
984 local i = start + integer_part:len()
985
986 --
987 -- Grab an optional decimal part
988 --
989 local decimal_part = text:match('^%.%d+', i) or ""
990
991 i = i + decimal_part:len()
992
993 --
994 -- Grab an optional exponential part
995 --
996 local exponent_part = text:match('^[eE][-+]?%d+', i) or ""
997
998 i = i + exponent_part:len()
999
1000 local full_number_text = integer_part .. decimal_part .. exponent_part
1001 local as_number = tonumber(full_number_text)
1002
1003 if not as_number then
1004 self:onDecodeError("bad number", text, start, etc)
1005 end
1006
1007 return as_number, i
1008end
1009
1010
1011local function grok_string(self, text, start, etc)
1012
1013 if text:sub(start,start) ~= '"' then
1014 self:onDecodeError("expected string's opening quote", text, start, etc)
1015 end
1016
1017 local i = start + 1 -- +1 to bypass the initial quote
1018 local text_len = text:len()
1019 local VALUE = ""
1020 while i <= text_len do
1021 local c = text:sub(i,i)
1022 if c == '"' then
1023 return VALUE, i + 1
1024 end
1025 if c ~= '\\' then
1026 VALUE = VALUE .. c
1027 i = i + 1
1028 elseif text:match('^\\b', i) then
1029 VALUE = VALUE .. "\b"
1030 i = i + 2
1031 elseif text:match('^\\f', i) then
1032 VALUE = VALUE .. "\f"
1033 i = i + 2
1034 elseif text:match('^\\n', i) then
1035 VALUE = VALUE .. "\n"
1036 i = i + 2
1037 elseif text:match('^\\r', i) then
1038 VALUE = VALUE .. "\r"
1039 i = i + 2
1040 elseif text:match('^\\t', i) then
1041 VALUE = VALUE .. "\t"
1042 i = i + 2
1043 else
1044 local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
1045 if hex then
1046 i = i + 6 -- bypass what we just read
1047
1048 -- We have a Unicode codepoint. It could be standalone, or if in the proper range and
1049 -- followed by another in a specific range, it'll be a two-code surrogate pair.
1050 local codepoint = tonumber(hex, 16)
1051 if codepoint >= 0xD800 and codepoint <= 0xDBFF then
1052 -- it's a hi surrogate... see whether we have a following low
1053 local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
1054 if lo_surrogate then
1055 i = i + 6 -- bypass the low surrogate we just read
1056 codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16)
1057 else
1058 -- not a proper low, so we'll just leave the first codepoint as is and spit it out.
1059 end
1060 end
1061 VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint)
1062
1063 else
1064
1065 -- just pass through what's escaped
1066 VALUE = VALUE .. text:match('^\\(.)', i)
1067 i = i + 2
1068 end
1069 end
1070 end
1071
1072 self:onDecodeError("unclosed string", text, start, etc)
1073end
1074
1075local function skip_whitespace(text, start)
1076
1077 local match_start, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2
1078 if match_end then
1079 return match_end + 1
1080 else
1081 return start
1082 end
1083end
1084
1085local grok_one -- assigned later
1086
1087local function grok_object(self, text, start, etc)
1088 if not text:sub(start,start) == '{' then
1089 self:onDecodeError("expected '{'", text, start, etc)
1090 end
1091
1092 local i = skip_whitespace(text, start + 1) -- +1 to skip the '{'
1093
1094 local VALUE = self.strictTypes and self:newObject { } or { }
1095
1096 if text:sub(i,i) == '}' then
1097 return VALUE, i + 1
1098 end
1099 local text_len = text:len()
1100 while i <= text_len do
1101 local key, new_i = grok_string(self, text, i, etc)
1102
1103 i = skip_whitespace(text, new_i)
1104
1105 if text:sub(i, i) ~= ':' then
1106 self:onDecodeError("expected colon", text, i, etc)
1107 end
1108
1109 i = skip_whitespace(text, i + 1)
1110
1111 local val, new_i = grok_one(self, text, i)
1112
1113 VALUE[key] = val
1114
1115 --
1116 -- Expect now either '}' to end things, or a ',' to allow us to continue.
1117 --
1118 i = skip_whitespace(text, new_i)
1119
1120 local c = text:sub(i,i)
1121
1122 if c == '}' then
1123 return VALUE, i + 1
1124 end
1125
1126 if text:sub(i, i) ~= ',' then
1127 self:onDecodeError("expected comma or '}'", text, i, etc)
1128 end
1129
1130 i = skip_whitespace(text, i + 1)
1131 end
1132
1133 self:onDecodeError("unclosed '{'", text, start, etc)
1134end
1135
1136local function grok_array(self, text, start, etc)
1137 if not text:sub(start,start) == '[' then
1138 self:onDecodeError("expected '['", text, start, etc)
1139 end
1140
1141 local i = skip_whitespace(text, start + 1) -- +1 to skip the '['
1142 local VALUE = self.strictTypes and self:newArray { } or { }
1143 if text:sub(i,i) == ']' then
1144 return VALUE, i + 1
1145 end
1146
1147 local VALUE_INDEX = 1
1148
1149 local text_len = text:len()
1150 while i <= text_len do
1151 local val, new_i = grok_one(self, text, i)
1152
1153 -- can't table.insert(VALUE, val) here because it's a no-op if val is nil
1154 VALUE[VALUE_INDEX] = val
1155 VALUE_INDEX = VALUE_INDEX + 1
1156
1157 i = skip_whitespace(text, new_i)
1158
1159 --
1160 -- Expect now either ']' to end things, or a ',' to allow us to continue.
1161 --
1162 local c = text:sub(i,i)
1163 if c == ']' then
1164 return VALUE, i + 1
1165 end
1166 if text:sub(i, i) ~= ',' then
1167 self:onDecodeError("expected comma or '['", text, i, etc)
1168 end
1169 i = skip_whitespace(text, i + 1)
1170 end
1171 self:onDecodeError("unclosed '['", text, start, etc)
1172end
1173
1174
1175grok_one = function(self, text, start, etc)
1176 -- Skip any whitespace
1177 start = skip_whitespace(text, start)
1178
1179 if start > text:len() then
1180 self:onDecodeError("unexpected end of string", text, nil, etc)
1181 end
1182
1183 if text:find('^"', start) then
1184 return grok_string(self, text, start, etc)
1185
1186 elseif text:find('^[-0123456789 ]', start) then
1187 return grok_number(self, text, start, etc)
1188
1189 elseif text:find('^%{', start) then
1190 return grok_object(self, text, start, etc)
1191
1192 elseif text:find('^%[', start) then
1193 return grok_array(self, text, start, etc)
1194
1195 elseif text:find('^true', start) then
1196 return true, start + 4
1197
1198 elseif text:find('^false', start) then
1199 return false, start + 5
1200
1201 elseif text:find('^null', start) then
1202 return nil, start + 4
1203
1204 else
1205 self:onDecodeError("can't parse JSON", text, start, etc)
1206 end
1207end
1208
1209function OBJDEF:decode(text, etc)
1210 if type(self) ~= 'table' or self.__index ~= OBJDEF then
1211 OBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc)
1212 end
1213
1214 if text == nil then
1215 self:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc)
1216 elseif type(text) ~= 'string' then
1217 self:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc)
1218 end
1219
1220 if text:match('^%s*$') then
1221 return nil
1222 end
1223
1224 if text:match('^%s*<') then
1225 -- Can't be JSON... we'll assume it's HTML
1226 self:onDecodeOfHTMLError(string.format("html passed to JSON:decode()"), text, nil, etc)
1227 end
1228
1229 --
1230 -- Ensure that it's not UTF-32 or UTF-16.
1231 -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3),
1232 -- but this package can't handle them.
1233 --
1234 if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then
1235 self:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc)
1236 end
1237
1238 local success, value = pcall(grok_one, self, text, 1, etc)
1239
1240 if success then
1241 return value
1242 else
1243 -- if JSON:onDecodeError() didn't abort out of the pcall, we'll have received the error message here as "value", so pass it along as an assert.
1244 if self.assert then
1245 self.assert(false, value)
1246 else
1247 assert(false, value)
1248 end
1249 -- and if we're still here, return a nil and throw the error message on as a second arg
1250 return nil, value
1251 end
1252end
1253
1254local function backslash_replacement_function(c)
1255 if c == "\n" then
1256 return "\\n"
1257 elseif c == "\r" then
1258 return "\\r"
1259 elseif c == "\t" then
1260 return "\\t"
1261 elseif c == "\b" then
1262 return "\\b"
1263 elseif c == "\f" then
1264 return "\\f"
1265 elseif c == '"' then
1266 return '\\"'
1267 elseif c == '\\' then
1268 return '\\\\'
1269 else
1270 return string.format("\\u%04x", c:byte())
1271 end
1272end
1273
1274local chars_to_be_escaped_in_JSON_string
1275 = '['
1276 .. '"' -- class sub-pattern to match a double quote
1277 .. '%\\' -- class sub-pattern to match a backslash
1278 .. '%z' -- class sub-pattern to match a null
1279 .. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters
1280 .. ']'
1281
1282local function json_string_literal(value)
1283 local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function)
1284 return '"' .. newval .. '"'
1285end
1286
1287local function object_or_array(self, T, etc)
1288 --
1289 -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON
1290 -- object. If there are only numbers, it's a JSON array.
1291 --
1292 -- If we'll be converting to a JSON object, we'll want to sort the keys so that the
1293 -- end result is deterministic.
1294 --
1295 local string_keys = { }
1296 local number_keys = { }
1297 local number_keys_must_be_strings = false
1298 local maximum_number_key
1299
1300 for key in pairs(T) do
1301 if type(key) == 'string' then
1302 table.insert(string_keys, key)
1303 elseif type(key) == 'number' then
1304 table.insert(number_keys, key)
1305 if key <= 0 or key >= math.huge then
1306 number_keys_must_be_strings = true
1307 elseif not maximum_number_key or key > maximum_number_key then
1308 maximum_number_key = key
1309 end
1310 else
1311 self:onEncodeError("can't encode table with a key of type " .. type(key), etc)
1312 end
1313 end
1314
1315 if #string_keys == 0 and not number_keys_must_be_strings then
1316 --
1317 -- An empty table, or a numeric-only array
1318 --
1319 if #number_keys > 0 then
1320 return nil, maximum_number_key -- an array
1321 elseif tostring(T) == "JSON array" then
1322 return nil
1323 elseif tostring(T) == "JSON object" then
1324 return { }
1325 else
1326 -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects
1327 return nil
1328 end
1329 end
1330
1331 table.sort(string_keys)
1332
1333 local map
1334 if #number_keys > 0 then
1335 --
1336 -- If we're here then we have either mixed string/number keys, or numbers inappropriate for a JSON array
1337 -- It's not ideal, but we'll turn the numbers into strings so that we can at least create a JSON object.
1338 --
1339
1340 if JSON.noKeyConversion then
1341 self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc)
1342 end
1343
1344 --
1345 -- Have to make a shallow copy of the source table so we can remap the numeric keys to be strings
1346 --
1347 map = { }
1348 for key, val in pairs(T) do
1349 map[key] = val
1350 end
1351
1352 table.sort(number_keys)
1353
1354 --
1355 -- Throw numeric keys in there as strings
1356 --
1357 for _, number_key in ipairs(number_keys) do
1358 local string_key = tostring(number_key)
1359 if map[string_key] == nil then
1360 table.insert(string_keys , string_key)
1361 map[string_key] = T[number_key]
1362 else
1363 self:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc)
1364 end
1365 end
1366 end
1367
1368 return string_keys, nil, map
1369end
1370
1371--
1372-- Encode
1373--
1374local encode_value -- must predeclare because it calls itself
1375function encode_value(self, value, parents, etc, indent) -- non-nil indent means pretty-printing
1376
1377 if value == nil then
1378 return 'null'
1379
1380 elseif type(value) == 'string' then
1381 return json_string_literal(value)
1382
1383 elseif type(value) == 'number' then
1384 if value ~= value then
1385 --
1386 -- NaN (Not a Number).
1387 -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option.
1388 --
1389 return "null"
1390 elseif value >= math.huge then
1391 --
1392 -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should
1393 -- really be a package option. Note: at least with some implementations, positive infinity
1394 -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is.
1395 -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">="
1396 -- case first.
1397 --
1398 return "1e+9999"
1399 elseif value <= -math.huge then
1400 --
1401 -- Negative infinity.
1402 -- JSON has no INF, so we have to fudge the best we can. This should really be a package option.
1403 --
1404 return "-1e+9999"
1405 else
1406 return tostring(value)
1407 end
1408
1409 elseif type(value) == 'boolean' then
1410 return tostring(value)
1411
1412 elseif type(value) ~= 'table' then
1413 self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc)
1414
1415 else
1416 --
1417 -- A table to be converted to either a JSON object or array.
1418 --
1419 local T = value
1420
1421 if parents[T] then
1422 self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)
1423 else
1424 parents[T] = true
1425 end
1426
1427 local result_value
1428
1429 local object_keys, maximum_number_key, map = object_or_array(self, T, etc)
1430 if maximum_number_key then
1431 --
1432 -- An array...
1433 --
1434 local ITEMS = { }
1435 for i = 1, maximum_number_key do
1436 table.insert(ITEMS, encode_value(self, T[i], parents, etc, indent))
1437 end
1438
1439 if indent then
1440 result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]"
1441 else
1442 result_value = "[" .. table.concat(ITEMS, ",") .. "]"
1443 end
1444
1445 elseif object_keys then
1446 --
1447 -- An object
1448 --
1449 local TT = map or T
1450
1451 if indent then
1452
1453 local KEYS = { }
1454 local max_key_length = 0
1455 for _, key in ipairs(object_keys) do
1456 local encoded = encode_value(self, tostring(key), parents, etc, "")
1457 max_key_length = math.max(max_key_length, #encoded)
1458 table.insert(KEYS, encoded)
1459 end
1460 local key_indent = indent .. " "
1461 local subtable_indent = indent .. string.rep(" ", max_key_length + 2 + 4)
1462 local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s"
1463
1464 local COMBINED_PARTS = { }
1465 for i, key in ipairs(object_keys) do
1466 local encoded_val = encode_value(self, TT[key], parents, etc, subtable_indent)
1467 table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val))
1468 end
1469 result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}"
1470
1471 else
1472
1473 local PARTS = { }
1474 for _, key in ipairs(object_keys) do
1475 local encoded_val = encode_value(self, TT[key], parents, etc, indent)
1476 local encoded_key = encode_value(self, tostring(key), parents, etc, indent)
1477 table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val))
1478 end
1479 result_value = "{" .. table.concat(PARTS, ",") .. "}"
1480
1481 end
1482 else
1483 --
1484 -- An empty array/object... we'll treat it as an array, though it should really be an option
1485 --
1486 result_value = "[]"
1487 end
1488
1489 parents[T] = false
1490 return result_value
1491 end
1492end
1493
1494
1495function OBJDEF:encode(value, etc)
1496 if type(self) ~= 'table' or self.__index ~= OBJDEF then
1497 OBJDEF:onEncodeError("JSON:encode must be called in method format", etc)
1498 end
1499 return encode_value(self, value, {}, etc, nil)
1500end
1501
1502function OBJDEF:encode_pretty(value, etc)
1503 if type(self) ~= 'table' or self.__index ~= OBJDEF then
1504 OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc)
1505 end
1506 return encode_value(self, value, {}, etc, "")
1507end
1508
1509function OBJDEF.__tostring()
1510 return "JSON encode/decode package"
1511end
1512
1513OBJDEF.__index = OBJDEF
1514
1515function OBJDEF:new(args)
1516 local new = { }
1517
1518 if args then
1519 for key, val in pairs(args) do
1520 new[key] = val
1521 end
1522 end
1523
1524 return setmetatable(new, OBJDEF)
1525end
1526
1527 return OBJDEF:new()
1528
1529--
1530-- Version history:
1531--
1532-- 20140418.11 JSON nulls embedded within an array were being ignored, such that
1533-- ["1",null,null,null,null,null,"seven"],
1534-- would return
1535-- {1,"seven"}
1536-- It's now fixed to properly return
1537-- {1, nil, nil, nil, nil, nil, "seven"}
1538-- Thanks to "haddock" for catching the error.
1539--
1540-- 20140116.10 The user's JSON.assert() wasn't always being used. Thanks to "blue" for the heads up.
1541--
1542-- 20131118.9 Update for Lua 5.3... it seems that tostring(2/1) produces "2.0" instead of "2",
1543-- and this caused some problems.
1544--
1545-- 20131031.8 Unified the code for encode() and encode_pretty(); they had been stupidly separate,
1546-- and had of course diverged (encode_pretty didn't get the fixes that encode got, so
1547-- sometimes produced incorrect results; thanks to Mattie for the heads up).
1548--
1549-- Handle encoding tables with non-positive numeric keys (unlikely, but possible).
1550--
1551-- If a table has both numeric and string keys, or its numeric keys are inappropriate
1552-- (such as being non-positive or infinite), the numeric keys are turned into
1553-- string keys appropriate for a JSON object. So, as before,
1554-- JSON:encode({ "one", "two", "three" })
1555-- produces the array
1556-- ["one","two","three"]
1557-- but now something with mixed key types like
1558-- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" }))
1559-- instead of throwing an error produces an object:
1560-- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"}
1561--
1562-- To maintain the prior throw-an-error semantics, set
1563-- JSON.noKeyConversion = true
1564--
1565-- 20131004.7 Release under a Creative Commons CC-BY license, which I should have done from day one, sorry.
1566--
1567-- 20130120.6 Comment update: added a link to the specific page on my blog where this code can
1568-- be found, so that folks who come across the code outside of my blog can find updates
1569-- more easily.
1570--
1571-- 20111207.5 Added support for the 'etc' arguments, for better error reporting.
1572--
1573-- 20110731.4 More feedback from David Kolf on how to make the tests for Nan/Infinity system independent.
1574--
1575-- 20110730.3 Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules:
1576--
1577-- * When encoding lua for JSON, Sparse numeric arrays are now handled by
1578-- spitting out full arrays, such that
1579-- JSON:encode({"one", "two", [10] = "ten"})
1580-- returns
1581-- ["one","two",null,null,null,null,null,null,null,"ten"]
1582--
1583-- In 20100810.2 and earlier, only up to the first non-null value would have been retained.
1584--
1585-- * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999".
1586-- Version 20100810.2 and earlier created invalid JSON in both cases.
1587--
1588-- * Unicode surrogate pairs are now detected when decoding JSON.
1589--
1590-- 20100810.2 added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding
1591--
1592-- 20100731.1 initial public release
1593--
1594
1595
1596end