Skip to content

Instantly share code, notes, and snippets.

@davidfstr
Created January 18, 2015 20:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidfstr/0f212ddf160b2f776884 to your computer and use it in GitHub Desktop.
Save davidfstr/0f212ddf160b2f776884 to your computer and use it in GitHub Desktop.
MediaQueue 1.0
/*
* 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