Created
January 18, 2015 20:50
-
-
Save davidfstr/0f212ddf160b2f776884 to your computer and use it in GitHub Desktop.
MediaQueue 1.0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* MediaQueue | |
* | |
* A system for quickly searching for media items (movies, TV series, anime, books) | |
* in various different media sources. | |
* | |
* If you're reading this file then you might be interesting in either understanding | |
* how it works or extending it (especially to add new kinds of media sources). | |
* Useful starting points: | |
* | |
* + Main entry points: | |
* - onEditWithAuthorization - Main entrypoint. Called whenever a cell in the spreadsheet is changed. | |
* - onOpen - Sets up the menus when the spreadsheet is initially opened. | |
* | |
* + Adding new types of media sources: | |
* - computeSourcesToSearch - Determines names of sources to be searched based on the | |
* search type (ex: "Streaming") and item type (ex: "M", "A"). | |
* - SOURCES - Maps source names to source search functions. | |
* - search*(query) - Source search functions. Always takes a query and returns result objects. | |
* | |
* + Useful utility functions & libraries | |
* - parseHtmlFromUrl - Downloads and parses an HTML document. | |
* - $ - Locates elements within an HTML or XML document using CSS selectors. Similar to jQuery. | |
*/ | |
// === Events === | |
function onOpen() { | |
var ui = SpreadsheetApp.getUi(); | |
ui.createMenu('MediaQueue') | |
.addItem('Find link...', 'searchForLinks') | |
.addSeparator() | |
.addItem('Settings...', 'showHideSettings') | |
.addToUi(); | |
} | |
// NOTE: This function is registered as a manual trigger for the "on edit" spreadsheet event. | |
// The regular default onEdit() handler isn't good enough because it doesn't allow | |
// performing actions that require authorization. | |
function onEditWithAuthorization(e) { | |
var sheet = SpreadsheetApp.getActiveSheet(); | |
var sheetType = getSheetType(sheet); | |
if (sheetType === 'queue') { | |
var typedValue = e.range.getValue(); | |
if (typedValue.length >= 1 && typedValue.charAt(0) === '.') { | |
searchForLinks(); | |
} | |
} else if (sheetType === 'search') { | |
if (isInputCell(e.range)) { | |
SearchSheet_submitInputCell(e.range); | |
} | |
} else if (sheetType === 'settings') { | |
if (isInputCell(e.range)) { | |
e.range.setValue(''); | |
hideSettings(); | |
} | |
} | |
} | |
// === Sheets === | |
var INPUT_CELL_BACKGROUND = '#ffff00'; | |
function getSheetType(sheet) { | |
var sheetName = sheet.getName(); | |
if (sheetName === 'Queue') { | |
return 'queue'; | |
} else if (sheetName.indexOf('Search ') === 0) { | |
return 'search'; | |
} else if (sheetName === 'Settings') { | |
return 'settings'; | |
} else { | |
return 'unknown'; | |
} | |
} | |
function getQueueSheet() { | |
return SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Queue'); | |
} | |
function getSettingsSheet() { | |
return SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Settings'); | |
} | |
function isInputCell(range) { | |
return range.getBackground() === INPUT_CELL_BACKGROUND; | |
} | |
function findInputCell(sheet) { | |
for (var row = 1; row <= sheet.getLastRow(); row++) { | |
if (isInputCell(sheet.getRange(row, 1))) { | |
return sheet.getRange(row, 1); | |
} | |
} | |
return null; | |
} | |
// === Queue Sheet === | |
function searchForLinks() { | |
var sheet = SpreadsheetApp.getActiveSheet(); | |
if (getSheetType(sheet) !== 'queue') { | |
var ui = SpreadsheetApp.getUi(); | |
ui.alert('Please switch to the Queue sheet to run this command.'); | |
return; | |
} | |
// Identify selected queue item and requested search type | |
var activeCell = sheet.getActiveCell(); | |
var itemTypes = ('' + sheet.getRange(activeCell.getRow(), 1).getValue()).split(','); | |
var itemName = sheet.getRange(activeCell.getRow(), 2).getValue(); | |
var searchType = sheet.getRange(1, activeCell.getColumn()).getValue(); | |
// If this search was activated by typing ".QUERY", use "QUERY" instead of the item name for the search | |
var typedValue = activeCell.getValue(); | |
if (typedValue.length >= 2 && typedValue.charAt(0) == '.') { | |
itemName = typedValue.substring(1); | |
} | |
// Identify promising media sources | |
var sources = computeSourcesToSearch(searchType, itemTypes); | |
if (sources === null) { | |
// Not in a valid column | |
return; | |
} | |
Logger.log('Searching for: ' + itemName); | |
// Print header | |
var self = SearchSheet_create(itemName); | |
// Perform searches on each source and print results | |
for (var i = 0; i < sources.length; i++) { | |
var source = sources[i]; | |
SearchSheet_printSource(self, source); | |
// (Display progress during the search) | |
SpreadsheetApp.flush(); | |
Logger.log('Searching source: ' + source); | |
var results; | |
var searchFunc = SOURCES[source]; | |
if (searchFunc) { | |
try { | |
results = searchFunc(itemName); | |
} catch (e) { | |
if (typeof(e) === 'string') { | |
results = [{ | |
name: 'ERROR: ' + e, | |
url: 'about:blank' | |
}]; | |
} else { | |
results = [{ | |
name: 'ERROR: Problem scraping source: ' + e.fileName + ':' + e.lineNumber + ' - ' + e.message, | |
url: 'about:blank' | |
}]; | |
} | |
} | |
} else { | |
results = [{ | |
name: 'ERROR: Don\'t know how to search this source.', | |
url: 'about:blank' | |
}]; | |
} | |
if (results.length === 0) { | |
SearchSheet_print(self, '', 'No results.'); | |
} else { | |
for (var j = 0; j < results.length; j++) { | |
var result = results[j]; | |
SearchSheet_printChoice(self, result.name, {link: result.url}); | |
} | |
} | |
} | |
Logger.log('Search complete'); | |
// Print footer #1 | |
SearchSheet_printSection(self, 'Other Choices'); | |
SearchSheet_printChoice(self, 'Fill in with "N/A"', {id: 'N'}); | |
SearchSheet_printChoice(self, 'Cancel', {id: 'X'}); | |
// Print footer #2 | |
SearchSheet_print(self, '', ''); | |
SearchSheet_print(self, '', '<-- Type your choice here', {style: 'input', selectInput: true}); | |
SearchSheet_print(self, '', ''); | |
SearchSheet_print(self, '%C', activeCell.getA1Notation(), {style: 'gray'}); | |
} | |
function computeSourcesToSearch(searchType, itemTypes) { | |
var sources = []; | |
if (searchType === 'Info') { | |
if (itemTypes.indexOf('A') !== -1) { | |
sources.push('THEM Anime'); | |
sources.push('Anime Planet'); | |
} | |
if (itemTypes.indexOf('M') !== -1) { | |
sources.push('Rotten Tomatoes'); | |
sources.push('IMDB'); | |
} | |
sources.push('Wikipedia'); | |
sources.push('Google'); | |
} else if (searchType === 'Stream') { | |
if (itemTypes.indexOf('A') !== -1) { | |
sources.push('KissAnime'); | |
//sources.push('Crunchyroll'); // has very few titles | |
} | |
sources.push('Netflix (Streaming)'); | |
} else if (searchType === 'Download') { | |
if (itemTypes.indexOf('A') !== -1) { | |
sources.push('BakaBT'); | |
sources.push('NyaaTorrents'); | |
} | |
sources.push('The Pirate Bay'); | |
} else if (searchType === 'Pickup') { | |
if (itemTypes.indexOf('B') !== -1) { | |
sources.push('Barnes & Noble'); | |
} | |
sources.push('Seattle Public Library'); | |
} else if (searchType === 'Buy') { | |
sources.push('iTunes Store'); | |
sources.push('Amazon'); | |
} else { | |
// Not in a valid column | |
return null; | |
} | |
return sources; | |
} | |
// === Search Sheets === | |
function /*static*/ SearchSheet_create(query) { | |
var sheetName = 'Search for "' + query + '"'; | |
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); | |
var sheet = spreadsheet.getSheetByName(sheetName); | |
if (sheet) { | |
sheet.clear(); | |
sheet.activate(); | |
} else { | |
sheet = spreadsheet.insertSheet(sheetName, spreadsheet.getNumSheets()); | |
sheet.activate(); | |
} | |
sheet.setColumnWidth(1, 40); | |
sheet.getRange(1, 1, sheet.getMaxRows(), 1).setHorizontalAlignment('right'); | |
sheet.setFrozenRows(1); | |
// Immediately move active cell to position where errant typing won't cause problems later | |
sheet.setActiveRange(sheet.getRange(2, 2)); | |
var self = { | |
sheet: sheet, | |
nextRow: 1, | |
nextChoice: 1 | |
}; | |
SearchSheet_print(self, '%', sheetName, {style: 'bold'}); | |
return self; | |
} | |
function SearchSheet_print(self, leftSide, rightSide, kwargs) { | |
if (kwargs && kwargs.link) { | |
rightSide = '=HYPERLINK(' + quoteFormulaString(kwargs.link) + ',' + quoteFormulaString(rightSide) + ')'; | |
} | |
var range = self.sheet.getRange(self.nextRow, 1, 1, 2); | |
range.setValues([[leftSide, rightSide]]); | |
var styleOverride = kwargs && kwargs.style; | |
if (styleOverride === 'bold') { | |
range.setFontWeight('bold'); | |
} | |
if (styleOverride === 'input') { | |
range.getCell(1, 1).setBackground(INPUT_CELL_BACKGROUND); | |
} | |
if (styleOverride === 'gray') { | |
range.setFontColor('gray'); | |
} | |
if (kwargs && kwargs.selectInput) { | |
self.sheet.setActiveRange(range.getCell(1, 1)); | |
} | |
self.nextRow++; | |
} | |
function SearchSheet_printSection(self, sectionName) { | |
SearchSheet_print(self, '', ''); | |
SearchSheet_print(self, '', sectionName, {style: 'bold'}); | |
} | |
function SearchSheet_printSource(self, sourceName) { | |
SearchSheet_printSection(self, 'Source "' + sourceName + '":'); | |
} | |
function SearchSheet_printChoice(self, choiceName, kwargs) { | |
var choiceId = kwargs && kwargs.id; | |
if (!choiceId) { | |
choiceId = self.nextChoice; | |
self.nextChoice++; | |
} | |
var printKwargs = {}; | |
if (kwargs && kwargs.link) { | |
printKwargs.link = kwargs.link; | |
} | |
SearchSheet_print(self, choiceId, choiceName, printKwargs); | |
} | |
function /*static*/ SearchSheet_submitInputCell(inputCell) { | |
var choiceId = ('' + inputCell.getValue()).toUpperCase(); | |
if (choiceId === '!') { | |
// Ignore error sentinel (to avoid infinite loop) | |
return; | |
} | |
// Scan for (1) choice that matches the selected choice ID | |
// (2) saved queue cell location (in row '%C') | |
var choiceFormula = null; | |
var queueCellLocation = null; | |
var sheet = inputCell.getSheet(); | |
for (var row = 1; row <= sheet.getLastRow(); row++) { | |
if (isInputCell(sheet.getRange(row, 1))) { | |
continue; | |
} | |
var rowId = '' + sheet.getRange(row, 1).getValue(); | |
if (rowId === choiceId) { | |
choiceFormula = sheet.getRange(row, 2).getFormula(); | |
} | |
if (rowId === '%C') { | |
queueCellLocation = sheet.getRange(row, 2).getValue(); | |
} | |
} | |
if (choiceFormula === null || queueCellLocation === null) { | |
// Unable to find selected choice or saved data | |
inputCell.setValue('!'); | |
sheet.setActiveRange(inputCell); | |
return; | |
} | |
var newQueueCellValue; | |
if (choiceId == 'N') { | |
newQueueCellValue = 'N/A'; | |
} else if (choiceId == 'X') { | |
newQueueCellValue = '__CANCEL__'; | |
} else { | |
var choiceUrlQuoted = choiceFormula.replace(/^=HYPERLINK\(("([^"]|"")*"),.*$/, '$1'); | |
var choiceUrl = unquoteFormulaString(choiceUrlQuoted); | |
newQueueCellValue = choiceUrl; | |
} | |
// Insert selected choice back in the queue sheet and select it | |
var queueSheet = getQueueSheet(); | |
queueSheet.activate(); | |
var queueCell = queueSheet.getRange(queueCellLocation); | |
if (newQueueCellValue === '__CANCEL__') { | |
if (queueCell.getValue() === '.') { | |
queueCell.setValue(''); | |
} | |
} else { | |
queueCell.setValue(newQueueCellValue); | |
} | |
queueSheet.setActiveRange(queueCell); | |
// Delete the search sheet | |
sheet.getParent().deleteSheet(sheet); | |
} | |
// === Media Sources === | |
var SOURCES = { | |
'KissAnime': searchKissAnime, | |
'The Pirate Bay': searchOldPirateBay, | |
'Netflix (Streaming)': searchNetflixStreaming, | |
'Seattle Public Library': searchSeattlePublicLibrary, | |
'Barnes & Noble': searchBarnesAndNoble, | |
'Amazon': searchAmazon, | |
'BakaBT': searchBakaBT | |
}; | |
function searchKissAnime(query) { | |
var url = 'http://kissanime.com/Search/Anime?keyword=' + encodeURIComponent(query); | |
var root = parseHtmlFromUrl(url); | |
var resultNodes = $('.listing tr td a', [root]); | |
return resultNodes.map(function(resultNode) { | |
return { | |
name: resultNode.getText().trim(), | |
url: resolveHref(url, getHrefOfNode(resultNode)) | |
}; | |
}); | |
} | |
function searchOldPirateBay(query) { | |
var url = 'https://oldpiratebay.org/search.php?q=' + encodeURIComponent(query) + '&Torrent_sort=seeders.desc'; | |
var root = parseHtmlFromUrl(url); | |
// NOTE: The selector $('.title-row > a', [root]) really should work | |
// here but Google's XML parser is inserting a ton of random | |
// <em> elements into the tree which messes up that selector. | |
var results = []; | |
var rows = $('.table-torrents tbody tr', [root]); | |
rows.forEach(function(row) { | |
var titleRow = $('.title-row', [row])[0]; | |
// NOTE: Use getValue() instead of getText() for next 3 lines to workaround and ignore | |
// bizarre <em> elements inserted everywhere by the Google XML parser. | |
var seeders = $('.seeders-row', [row])[0].getValue().trim(); | |
var leechers = $('.leechers-row', [row])[0].getValue().trim(); | |
var size = $('.size-row', [row])[0].getValue().trim(); | |
var stats = '[' + seeders + ' S, ' + leechers + ' L, ' + size + ']'; | |
var links = $('a', [titleRow]); | |
links.forEach(function(link) { | |
var href = getHrefOfNode(link); | |
if (href.indexOf('/torrent/') === 0) { | |
results.push({ | |
name: stats + ' ' + $('span', [link])[0].getText().trim(), | |
url: resolveHref(url, href) | |
}); | |
} | |
}); | |
}); | |
results = limitResults(results, 5, url); | |
return results; | |
} | |
// Uses instantwatcher.com to search because it requires no authentication | |
// and automatically filters out stuff on Netflix that can't be streamed. | |
function searchNetflixStreaming(query) { | |
var url = 'http://instantwatcher.com/titles?q=' + encodeURIComponent(query) + '&search_episodes=0'; | |
var root = parseHtmlFromUrl(url); | |
var rows = $('#searchResults .title-detail-link', [root]); | |
var results = rows.map(function(row) { | |
return { | |
name: row.getText().trim(), | |
url: resolveHref(url, getHrefOfNode(row)) | |
}; | |
}); | |
results.push({ | |
name: '... see all Netflix titles (including DVD only)', | |
url: 'http://dvd.netflix.com/Search?oq=&ac_posn=&fcld=true&v1=' + encodeURIComponent(query) + '&search_submit=' | |
}); | |
return results; | |
} | |
function searchSeattlePublicLibrary(query) { | |
var url = 'http://seattle.bibliocommons.com/search?utf8=✓&t=title&search_category=title&q=' + encodeURIComponent(query) + '&commit=Search'; | |
var root = parseHtmlFromUrl(url); | |
var infos = $('.searchResults .listItem .info', [root]); | |
var results = infos.map(function(info) { | |
var href = getHrefOfNode($('.title a', [info])[0]); | |
var title = $('.title a', [info])[0].getText(); | |
var authorElems = $('.author a', [info]); | |
var author = authorElems.length > 0 ? authorElems[0].getValue() : '?'; // strip out span-level markup | |
var format = $('.format strong', [info])[0].getText(); | |
var availability; | |
var isAvailable = $('.item_available', [info]).length !== 0; | |
var isNotAvailable = $('.item_not_available', [info]).length !== 0; | |
if (isAvailable) { | |
availability = 'Available'; | |
} else if (isNotAvailable) { | |
availability = 'All copies in use'; | |
} else { | |
availability = 'Check availability'; // unknown | |
} | |
var holdPositionElems = $('.holdposition', [info]); | |
var holdPosition = holdPositionElems.length === 0 ? '' : holdPositionElems[0].getText().trim(); | |
return { | |
name: title + ' - ' + author + ' (' + format + ') [' + (holdPosition ? holdPosition : availability) + ']', | |
url: resolveHref(url, href) | |
}; | |
}); | |
results = limitResults(results, 5, url); | |
return results; | |
} | |
function searchBarnesAndNoble(query) { | |
var FAVORITE_ZIPCODE = getSettingValue('FAVORITE_ZIPCODE'); | |
if (!FAVORITE_ZIPCODE) { | |
throw 'This source requires that the "FAVORITE_ZIPCODE" setting be configured on the Settings sheet.'; | |
} | |
var url = 'http://www.barnesandnoble.com/s?store=book&title=' + encodeURIComponent(query); | |
var root = parseHtmlFromUrl(url); | |
var infos = $('#search-results-1 .result .details', [root]); | |
var results = infos.map(function(info) { | |
var href = getHrefOfNode($('.title', [info])[0]); | |
var title = $('.title', [info])[0].getValue(); // strip out span-level markup | |
var author = $('.contributor', [info])[0].getValue(); // strip out span-level markup | |
var ean = href.replace(/^.*\bean=([0-9]+)/, '$1'); // parse EAN from href | |
var bookSearchUrl = 'http://search.barnesandnoble.com/booksearch/store.asp?EAN=' + ean + '&zipcode=' + FAVORITE_ZIPCODE; | |
return { | |
name: title + ' - ' + author, | |
url: bookSearchUrl | |
}; | |
}); | |
results = limitResults(results, 5, url); | |
return results; | |
} | |
function searchAmazon(query) { | |
var url = 'http://www.amazon.com/s?field-keywords=' + encodeURIComponent(query); | |
var root = parseHtmlFromUrl(url); | |
var results = []; | |
var items = $('.s-result-item', [root]); | |
items.forEach(function(item) { | |
var detailLinks = $('.s-access-detail-page', [item]); | |
if (detailLinks.length === 0) { | |
// Could be an injected link to IMDB, or perhaps the page format changed | |
return; | |
} else { | |
noDetailLinks = false; | |
} | |
var detailLink = detailLinks[0]; | |
var href = getHrefOfNode(detailLink); | |
var titleAttr = detailLink.getAttribute('title'); | |
var title = titleAttr ? titleAttr.getValue() : '?'; | |
// NOTE: Sensitive to page layout. Amazon does not use semantic HTML. | |
var authorElems = $('.a-spacing-small .a-spacing-none .a-size-small', [item]); | |
var author = (authorElems.length >= 2) ? authorElems[1].getValue() : '?'; | |
// NOTE: Sensitive to page layout. Amazon does not use semantic HTML. | |
var formatElems = $('h3', [item]); | |
var formats = formatElems.map(function(elem) { return elem.getValue(); }); | |
results.push({ | |
name: title + ' - ' + author + ' [' + formats.join(', ') + ']', | |
url: resolveHref(url, href) | |
}); | |
}); | |
if (items.length >= 5 && results.length === 0) { | |
throw 'Several results found but none have identifiable detail links. Amazon page format likely changed.'; | |
} | |
results = limitResults(results, 5, url); | |
return results; | |
} | |
function searchBakaBT(query) { | |
var BAKABT_AUTH_COOKIE = getSettingValue('BAKABT_AUTH_COOKIE'); | |
if (!BAKABT_AUTH_COOKIE) { | |
throw 'This source requires that the "BAKABT_AUTH_COOKIE" setting be configured on the Settings sheet.'; | |
} | |
var url = 'http://bakabt.me/browse.php?q=' + encodeURIComponent(query); | |
var root = parseHtmlFromUrl(url, { | |
'headers': { | |
'Cookie': BAKABT_AUTH_COOKIE | |
} | |
}); | |
var torrentsElems = $('.torrents', [root]); | |
if (torrentsElems.length === 0) { | |
var suggestionElems = $('.suggestion', [root]); | |
if (suggestionElems.length > 0) { | |
throw 'BakaBT authorization cookie used by MediaQueue has expired. Please regenerate.'; | |
} else { | |
throw 'BakaBT torrent table not found. Page layout could have changed or authorization cookie may have expired'; | |
} | |
} | |
var results = $('.title', [torrentsElems[0]]).map(function(titleElem) { | |
var title = titleElem.getValue(); // strip out span-level markup | |
var href = getHrefOfNode(titleElem); | |
return { | |
name: title, | |
url: resolveHref(url, href) | |
}; | |
}); | |
return results; | |
} | |
// === Utility: Media Sources === | |
function limitResults(results, maxCount, moreUrl) { | |
if (results.length > maxCount) { | |
results = results.slice(0, maxCount); | |
results.push({ | |
name: '... and more', | |
url: moreUrl | |
}); | |
} | |
return results; | |
} | |
// === Utility: Spreadsheet === | |
function quoteFormulaString(value) { | |
value = '' + value; // convert to string | |
return '"' + value.replace(/"/g, '""') + '"'; | |
} | |
function unquoteFormulaString(quotedValue) { | |
return quotedValue.substring(1, quotedValue.length - 1).replace(/""/g, '"'); | |
} | |
// === Utility: HTML: General === | |
function parseHtmlFromUrl(url, /*optional*/ options) { | |
var page; | |
if (options) { | |
page = UrlFetchApp.fetch(url, options); | |
} else { | |
page = UrlFetchApp.fetch(url); | |
} | |
var doc = Xml.parse(page, true); // HACK: relies on deprecated API | |
var bodyHtml = doc.html.body.toXmlString(); | |
doc = XmlService.parse(bodyHtml); | |
var root = doc.getRootElement(); | |
return root; | |
} | |
function getHrefOfNode(node) { | |
var href; | |
var hrefAttr = node.getAttribute('href'); | |
if (hrefAttr == null) { | |
// HACK: Return bogus blank href if we can't find one | |
href = ''; | |
} else { | |
href = hrefAttr.getValue(); | |
} | |
return href; | |
} | |
// Converts an href to an absolute URL. | |
// HACK: Currently only understands site-relative and absolute URLs, since that's the only kind I've run into so far | |
function resolveHref(baseUrl, href) { | |
if (href.length >= 1 && href.charAt(0) == '/') { | |
// Site-relative URL | |
var protocolLim = baseUrl.indexOf('://'); | |
var domainLim = baseUrl.indexOf('/', protocolLim + '://'.length); | |
var domain = baseUrl.substring(0, domainLim); | |
return domain + href; | |
} else if (href.indexOf('://') !== -1) { | |
// Absolute URL | |
return href; | |
} else { | |
// Page-relative URL | |
// HACK: Don't know how to process a relative href. | |
// For now just return the original href even though that's not a valid absolute URL output. | |
return href; | |
} | |
} | |
// === Utility: HTML: CSS Selectors / Mini jQuery === | |
// Locates descendent elements using a CSS selector. | |
// (This behavior emulates the real jQuery library.) | |
// | |
// Note that only a restricted subset of the full CSS selector syntax is supported. | |
// In particular only '#ID', '.CLAZZ', 'TYPE', and '>' selectors are supported. | |
// Fancy stuff like chained '.CLAZZ1.CLAZZ2' and chained 'TYPE.CLAZZ' are not supported. | |
// | |
// ex: $('.listing tr td a', [elem1, elem2]) | |
function $(selector, parents) { | |
var selectorParts = selector.split(' '); | |
var directChildOnly = false; | |
for (var i = 0; i < selectorParts.length; i++) { | |
var selectorPart = selectorParts[i]; | |
if (selectorPart === '>') { | |
directChildOnly = true; | |
} else { | |
if (selectorPart.charAt(0) === '#') { | |
// ID-based selector part | |
parents = $id(selectorPart.substring(1), parents, directChildOnly); | |
} else if (selectorPart.charAt(0) === '.') { | |
// Class-based selector part | |
parents = $class(selectorPart.substring(1), parents, directChildOnly); | |
} else { | |
// Type-based selector part | |
parents = $type(selectorPart, parents, directChildOnly); | |
} | |
directChildOnly = false; | |
} | |
} | |
return parents; | |
} | |
// $('#ID', parents) == $id('ID', parents) | |
function $id(targetId, parents, /*optional*/ directChildOnly) { | |
var results = []; | |
for (var i = 0; i < parents.length; i++) { | |
var parent = parents[i]; | |
$id1(targetId, parent, results, directChildOnly); | |
} | |
return results; | |
} | |
function $id1(targetId, parent, results, /*optional*/ directChildOnly) { | |
var id; | |
var idAttr = parent.getAttribute('id'); | |
if (idAttr == null) { | |
id = ''; | |
} else { | |
id = idAttr.getValue(); | |
} | |
if (id == targetId) { | |
results.push(parent); | |
return; | |
} | |
if (directChildOnly) { return; } | |
// Recursively descend | |
var children = parent.getChildren(); | |
for (var j = 0; j < children.length; j++) { | |
var child = children[j]; | |
$id1(targetId, child, results); | |
} | |
} | |
// $('.CLAZZ', parents) == $class('CLAZZ', parents) | |
function $class(clazzElement, parents, /*optional*/ directChildOnly) { | |
var results = []; | |
for (var i = 0; i < parents.length; i++) { | |
var parent = parents[i]; | |
$class1(clazzElement, parent, results, directChildOnly); | |
} | |
return results; | |
} | |
function $class1(clazzElement, parent, results, /*optional*/ directChildOnly) { | |
var clazzList; | |
var clazzListAttr = parent.getAttribute('class'); | |
if (clazzListAttr == null) { | |
clazzList = []; | |
} else { | |
clazzList = clazzListAttr.getValue().split(' '); | |
} | |
if (clazzList.indexOf(clazzElement) !== -1) { | |
results.push(parent); | |
return; | |
} | |
if (directChildOnly) { return; } | |
// Recursively descend | |
var children = parent.getChildren(); | |
for (var j = 0; j < children.length; j++) { | |
var child = children[j]; | |
$class1(clazzElement, child, results); | |
} | |
} | |
// $('TYPE', parents) == $type('TYPE', parents) | |
function $type(typeName, parents, /*optional*/ directChildOnly) { | |
var results = []; | |
for (var i = 0; i < parents.length; i++) { | |
var parent = parents[i]; | |
$type1(typeName, parent, results, directChildOnly); | |
} | |
return results; | |
} | |
function $type1(typeName, parent, results, /*optional*/ directChildOnly) { | |
if (parent.getName() == typeName) { | |
results.push(parent); | |
return; | |
} | |
if (directChildOnly) { return; } | |
// Recursively descend | |
var children = parent.getChildren(); | |
for (var j = 0; j < children.length; j++) { | |
var child = children[j]; | |
$type1(typeName, child, results); | |
} | |
} | |
// === Settings Sheet === | |
function showHideSettings() { | |
if (getSheetType(SpreadsheetApp.getActiveSheet()) === 'settings') { | |
hideSettings(); | |
} else { | |
showSettings(); | |
} | |
} | |
function showSettings() { | |
var settingsSheet = getSettingsSheet(); | |
settingsSheet.activate(); | |
settingsSheet.setActiveRange(findInputCell(settingsSheet)); | |
} | |
function hideSettings() { | |
getQueueSheet().activate(); | |
getSettingsSheet().hideSheet(); | |
} | |
/** | |
* Gets the value of the specified setting, or '' if not found. | |
*/ | |
function getSettingValue(settingKey) { | |
var sheet = getSettingsSheet(); | |
for (var row = 1; row <= sheet.getLastRow(); row++) { | |
if ((sheet.getRange(row, 1).getValue() === '%') && | |
(sheet.getRange(row, 2).getValue() === settingKey)) | |
{ | |
return sheet.getRange(row, 3).getValue(); | |
} | |
} | |
return ''; // not found | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment