--[[ =head1 NAME applets.SlimBrowser.DB - Item database. =head1 DESCRIPTION This object is designed to store and manage the browsing data Jive receives. Conceptually, this data is a long list of items n1, n2, n3..., which is received by chunks. Each chunk is a table with many properties but of particular interest are: - count: indicates the number of items in the long list - offset: indicates the first index of the data in the item_obj array - item_obj: array of consecutive elements, from offset to offset+#item_obj - playlist_timestamp: timestamp of the data (optional) If count is 0, then the other fields are optional (and may not even be looked at). Fresh data always refreshes old data, except if it would be cost prohibitive to do so. There should be one DB per long list "type". If the count or the timestamp of the long list is different from the existing stored info, the existing info is discarded. =head1 SYNOPSIS TODO =head1 FUNCTIONS =cut --]] -- stuff we use local _assert, tonumber, tostring, type, ipairs = _assert, tonumber, tostring, type, ipairs local oo = require("loop.base") local RadioGroup = require("jive.ui.RadioGroup") local math = require("math") local debug = require("jive.utils.debug") local log = require("jive.utils.log").logger("applet.SlimBrowser.data") -- our class module(..., oo.class) local BLOCK_SIZE = 200 -- init -- creates an empty database object function __init(self, windowSpec) log:debug("DB:__init()") return oo.rawnew(self, { -- data store = {}, last_chunk = false, -- last_chunk received, to access other non DB fields -- major items extracted from data count = 0, -- =last_chunk.count, the total number of items in the long list ts = false, -- =last_chunk.timestamp, the timestamp of the current long list (if available) currentIndex = 0, -- =last_chunk.playlist_cur_index, index of the current song (if available) -- cache last_indexed_chunk = false, complete = false, -- windowSpec (to create labels in renderer) windowSpec = windowSpec, }) end function menuStyle(self) return self.windowSpec.menuStyle end function windowStyle(self) return self.windowSpec.windowStyle end function labelItemStyle(self) return self.windowSpec.labelItemStyle end function getBlockSize(self) return BLOCK_SIZE end -- getRadioGroup -- either returns self.radioGroup or creates and returns it function getRadioGroup(self) if not self.radioGroup then self.radioGroup = RadioGroup() end return self.radioGroup end -- status -- Update the DB status from the chunk. function updateStatus(self, chunk) -- sanity check on the chunk _assert(chunk["count"], "chunk must have count field") -- keep the chunk as header, in all cases self.last_chunk = chunk -- update currentIndex if we have it local currentIndex = chunk["playlist_cur_index"] if currentIndex then self.currentIndex = currentIndex + 1 end -- detect change that invalidates data local ts = chunk["playlist_timestamp"] or false -- get the count local cCount = tonumber( chunk["count"] ) local reset = false if cCount != self.count then -- count has changed, drop the data log:debug("..store invalid, different count") reset = true elseif ts and self.ts != ts then -- ts has changed, drop the data log:debug("..store invalid, different timestamp") reset = true end if reset then self.store = {} self.complete = false self.upCompleted = false self.downCompleted = false end -- update the window properties if chunk and chunk.window then local window = chunk.window if window.menuStyle then self.windowSpec.menuStyle = window.menuStyle .. "menu" self.windowSpec.labelItemStyle = window.menuStyle .. "item" end if window.windowStyle then self.windowSpec.windowStyle = window.windowStyle end end self.ts = ts self.count = cCount return reset end -- menuItems -- Stores the chunk in the DB and returns data suitable for the menu:setItems call function menuItems(self, chunk) log:debug(self, " menuItems()") -- we may be called with no chunk, f.e. when building the window if not chunk then return self.count end -- update the status updateStatus(self, chunk) -- fix offset, CLI is 0 based and we're 1-based local cFrom = 0 local cTo = 0 if self.count > 0 then _assert(chunk["item_loop"], "chunk must have item_loop field if count>0") _assert(chunk["offset"], "chunk must have offset field if count>0") cFrom = chunk["offset"] + 1 cTo = cFrom + #chunk["item_loop"] - 1 end -- store chunk local key = math.floor(cFrom/BLOCK_SIZE) -- helpful debug flags to show what chunk key and range is being loaded log:debug('********************************* loading key number ', key) log:debug('********************************* cFrom: ', cFrom) log:debug('********************************* cTo: ', cTo) self.store[key] = chunk["item_loop"] return self.count, cFrom, cTo end function chunk(self) return self.last_chunk end function playlistIndex(self) if self.ts then return self.currentIndex else return nil end end function item(self, index) local current = (index == self.currentIndex) index = index - 1 local key = math.modf(index / BLOCK_SIZE) local offset = math.fmod(index, BLOCK_SIZE) + 1 if not self.store[key] then return end return self.store[key][offset], current end function size(self) return self.count end -- the missing method's job is to identify the next chunk to load function missing(self, index) -- use our cached result if self.complete then log:debug(self, " complete (cached)") return end -- if index isn't defined we load first chunk, last chunk, then all middle chunks from the top down -- otherwise we load the chunk that contains index, then chunks on either side of it back-and-forth until top and bottom chunks are filled if not self.last_chunk or not self.last_chunk.count then return 0, BLOCK_SIZE end local count = tonumber(self.last_chunk.count) -- determine the key for the last chunk in the chunk list local lastKey = 0 if count > BLOCK_SIZE then lastKey = math.modf(count / BLOCK_SIZE) if lastKey * BLOCK_SIZE == count then lastKey = lastKey - 1 end end local firstChunkFrom = 0 local firstChunkKey = 0 if not index then -- load first chunk if we don't have it if not self.store[0] then return 0, BLOCK_SIZE end -- load the last chunk if we don't have it if not self.store[lastKey] then return lastKey * BLOCK_SIZE, BLOCK_SIZE end -- up is done, so start searching downward self.searchDirection = 'down' self.upCompleted = true end -- search for chunks to load -- get key for first chunk if we don't have it if index then firstChunkFrom = index - ( index % BLOCK_SIZE ) firstChunkKey = math.floor(firstChunkFrom/BLOCK_SIZE) end -- don't search down if the first chunk loaded is also the last chunk if firstChunkFrom + BLOCK_SIZE >= count then self.downCompleted = true self.searchDirection = 'up' end -- if both down and up are done, then we are done if self.downCompleted and self.upCompleted then -- if we reach here we're complete (for next time) log:debug(self, " scan complete (calculated)") self.complete = true return end -- go up and down around firstChunkKey until we have self.store[0] and self.store[lastKey] if not self.searchDirection then self.searchDirection = 'up' end if self.searchDirection == 'up' and not self.upCompleted then -- start with previous key to first chunk, go to beginning local fromKey, toKey, step = firstChunkKey-1, 0, -1 if not self.downCompleted then self.searchDirection = 'down' end -- search for chunks to load for key = fromKey, toKey, step do if not self.store[key] then local thisChunkFrom = key * BLOCK_SIZE if key == toKey then self.upCompleted = true end return thisChunkFrom, BLOCK_SIZE end end self.upCompleted = true end if self.searchDirection == 'down' and not self.downCompleted then if not self.upCompleted then self.searchDirection = 'up' end -- start with next key from first chunk, go to end local fromKey, toKey, step = firstChunkKey+1, lastKey, 1 for key = fromKey, toKey, step do if not self.store[key] then local thisChunkFrom = key * BLOCK_SIZE if key == toKey then self.downCompleted = true end return thisChunkFrom, BLOCK_SIZE end end self.downCompleted = true end end function __tostring(self) return "DB {" .. tostring(self.windowSpec.text) .. "}" end --[[ =head1 LICENSE Copyright 2007 Logitech. All Rights Reserved. This file is subject to the Logitech Public Source License Version 1.0. Please see the LICENCE file for details. =cut --]]