NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Overdrive Libraries & Retailers // @namespace http://home.comcast.net/~mailerdaemon // @include https://*.libraryreserve.com/*.htm // @include https://*.libraryreserve.com/*.htm?* // @include https://*.libraryreserve.com/*.htm#* // @include https://*.mediavending.com/*.htm // @include https://*.mediavending.com/*.htm?* // @include https://*.mediavending.com/*.htm#* // @include https://secure.contentreserve.com/* // @include https://marketplace.overdrive.com/* // @include https://www.overdrive.com/* // @include http://* // @version 3.5 // @grant GM_log // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_openInTab // ==/UserScript== //"use strict"; //{ //{ AutoPager function isInAutoPagerHiddenbrowser() { var element = document.createElement("AutoPagerRequest"); element.setAttribute("requestType", "AutoPagerRequest"); element.setAttribute("requestName", "isInAutoPagerHiddenbrowser"); var parent = document.documentElement; if(!parent || !parent.appendChild) return; parent.appendChild(element); var evt = document.createEvent("Events"); evt.initEvent("AutoPagerRequest", true, false); element.dispatchEvent(evt); var inAp = element.getAttribute('response'); parent.removeChild(element); // GM_log(location + " = " + inAp) return inAp && (inAp === "true"); } /* AutoPager Rules <autopager> <site><urlPattern>^http://search.overdrive.com/(classic/|mobile/|)(retail/|)SearchResults\.asp$</urlPattern> <guid>DD3ECB0D-CCF3-1B00-2A0C-D9C2-9C16-5F52</guid> <margin>1</margin> <delaymsecs>200</delaymsecs> <owner>BlindWanderer</owner> <urlIsRegex>true</urlIsRegex> <enableJS>2</enableJS> <contentXPath>.//tr[td[1]/b/descendant-or-self::*[contains(text(), ':')] and count(td) <= 2]/ancestor::table[count(tbody/tr) <= 2 and position() < 3]/ancestor::table[1]</contentXPath> <testLink>http://search.overdrive.com/SearchResults.asp</testLink> <linkXPath>//a[(text()='Next >>')]</linkXPath> </site> </autopager> <autopager> <site><urlPattern>^http://search.overdrive.com/(classic/|mobile/|)(retail/|)SearchResults\.asp$</urlPattern> <guid>B70C72DF-987A-8A3F-0199-E529-14F6-692F</guid> <margin>1</margin> <owner>BlindWanderer</owner> <urlIsRegex>true</urlIsRegex> <enableJS>2</enableJS> <quickLoad>true</quickLoad> <needMouseDown>true</needMouseDown> <contentXPath>//table[tbody/tr/td/a[@href='default.asp' and contains(text(), 'New Search...')]]</contentXPath> <testLink>http://search.overdrive.com/classic/SearchResults.asp</testLink> <linkXPath>//a[(text()='Next >>')]</linkXPath> <desc>AutoPager rule for search.overdrive.com</desc> </site> </autopager> <autopager> <site><urlPattern>.*\/en/SearchResults(UserRank|-Coll|)\.htm\?.*</urlPattern> <guid>8189B8DC-9FF9-9AF0-AE18-F7F2-5E56-3E88</guid> <margin>1</margin> <minipages>5</minipages> <owner>BlindWanderer</owner> <urlIsRegex>true</urlIsRegex> <enableJS>2</enableJS> <fixOverflow>true</fixOverflow> <contentXPath>//table[script[contains(text(),'g_bFutureOnSaleDate')]/following-sibling::tbody[position()=1]] | //script[contains(text(), ' szPrimaryCreatorID ')]/ancestor::table[1]</contentXPath> <testLink></testLink> <linkXPath>//a[text()="Next >"]</linkXPath> <desc></desc> </site> </autopager> <autopager> <site><urlPattern>https?://[^\/]+/[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}/[0-9]+/[0-9]+/en/SearchResults(UserRank|-Coll|)\.htm.*</urlPattern> <guid>20CD5595-4CCD-D656-4CE2-AFE4-BECC-B8BB</guid> <margin>1</margin> <owner>BlindWanderer</owner> <urlIsRegex>true</urlIsRegex> <enableJS>false</enableJS> <quickLoad>true</quickLoad> <contentXPath>//table[tbody/tr/td/a/img[@data-crid]]</contentXPath> <contentXPath>//div[@id='main_content']</contentXPath> <testLink>http://www.listennj.com/B0381685-4C4A-4D38-A209-7DE8CEDC60E3/10/427/en/SearchResults.htm?SearchID=3090895s</testLink> <linkXPath>//a[(text()='Next >') and starts-with(@href,'SearchResults.htm')]</linkXPath> <desc>AutoPager rule for www.listennj.com</desc> </site> </autopager> */ //} /* This is really unsafe! Note the inline br tag! http://search.overdrive.com/classic/ErrorCapture.asp?Number=201&Message=Unable+to+perform+the+requested+query+on+the+database%2E%3Cbr%3EEXEC+sp%5FMediaLocatorDLRSearchQuery+%27Identifier%27%2C+N%27B006BF2Q2S%27%2C+%27coinSortTitle%27%2C+%27+ASC%27%2C+1%2C+10&Descrip=%5BMicrosoft%5D%5BODBC+SQL+Server+Driver%5DTimeout+expired http://search.overdrive.com/classic/ErrorCapture.asp?Number=201&Message=Unable+to+perform+the+requested+query+on+the+database.%3Cbr%3EEXEC+sp_MediaLocatorDLRSearchQuery+%27Title%27%2C+N%27wolf%27%2C+%27cifoDateReadyForSale%27%2C+%27+DESC%27%2C+1%2C+10&Descrip=[Microsoft][ODBC+SQL+Server+Driver][SQL+Server]Column+%27ContentInventoryFormat.cifoDateReadyForSale%27+is+invalid+in+the+select+list+because+it+is+not+contained+in+either+an+aggregate+function+or+the+GROUP+BY+clause. Amazing what you can find in the google cache. Saving them for a later date. Tables: ContentInventory ContentInventoryFormat ContentInventoryFormatSrp ListFormatType ListPublisher ListImprint ListContentType ListMinSoftwareVersion ListCurrency ListSecurityLevel ContentMarketingImage ContentMarketingImageAlt Unable to perform the requested query on the database. SELECT ContentInventory.acacAccessID, coinTitle, coinSubtitle, coinEdition, coinSeries, coinCoverage, coinAgeGroup, lpubPublisherName, dbo.f_GetGradeRange(ContentInventory.coinReserveID) AS GradeRange, dbo.f_GetAwards(ContentInventory.coinReserveID) AS Awards, limpImprintName, coinPubDate, lctyContentTypeDesc, coinWorldWideRightsFlag, cifoOnSaleDate, lffaMinFormFactorID, lffaRecomFormFactorID, lmsvMinSoftwareVersionDesc, (cifoSrp * lcurExchangeRateToUSD) AS cifoSrp, cifsSrp = CASE WHEN cifsSrp IS NOT NULL THEN (cifsSrp * lcurExchangeRateToUSD) ELSE '0' END, cifoContentDuration, cifoDistributorDiscount, cmimFileName, cmiaFileName, cifoDrmInfo, ContentInventoryFormat.lcurCurrencyCode, lcurCurrencyName, lsleSecurityLevelDesc, cifoContentFileSize, lftyFormatTypeDesc, dbo.getRetailerUrls(ContentInventory.coinReserveID, ContentInventoryFormat.lftyFormatTypeID) AS Retailers, cifoBundleFlag, dbo.getLanguage(ContentInventory.coinReserveID) AS Languages FROM ContentInventory INNER JOIN ContentInventoryFormat ON ContentInventory.coinReserveID = ContentInventoryFormat.coinReserveID INNER JOIN ListFormatType ON ContentInventoryFormat.lftyFormatTypeID = ListFormatType.lftyFormatTypeID LEFT OUTER JOIN ContentInventoryFormatSrp ON ContentInventoryFormat.coinReserveID = ContentInventoryFormatSrp.coinReserveID AND ContentInventoryFormat.lftyFormatTypeID = ContentInventoryFormatSrp.lftyFormatTypeID AND ContentInventoryFormat.lcurCurrencyCode = ContentInventoryFormatSrp.lcurCurrencyCode AND lstySrpTypeID = 2 LEFT OUTER JOIN ListPublisher ON ContentInventory.lpubPublisherID = ListPublisher.lpubPublisherID LEFT OUTER JOIN ListImprint ON ContentInventory.limpImprintID = ListImprint.limpImprintID LEFT OUTER JOIN ListContentType ON ContentInventory.lctyContentTypeID = ListContentType.lctyContentTypeID LEFT OUTER JOIN ListMinSoftwareVersion ON ContentInventoryFormat.lmsvMinSoftwareVersionID = ListMinSoftwareVersion.lmsvMinSoftwareVersionID LEFT OUTER JOIN ListCurrency ON ContentInventoryFormat.lcurCurrencyCode = ListCurrency.lcurCurrencyCode LEFT OUTER JOIN ListSecurityLevel ON ContentInventoryFormat.lsleSecurityLevelID = ListSecurityLevel.lsleSecurityLevelID LEFT OUTER JOIN ContentMarketingImage ON ContentInventory.coinReserveID = ContentMarketingImage.coinReserveID AND lityImageTypeID = 100 LEFT OUTER JOIN ContentMarketingImageAlt ON ContentInventory.coinReserveID = ContentMarketingImageAlt.coinReserveID AND ContentMarketingImageAlt.lityImageTypeID = 100 WHERE ContentInventory.coinReserveID = '{5D1016B6-CCB3-42B3-98EC-E8E8C8DEF694}' AND ContentInventoryFormat.lftyFormatTypeID = NaN http://search.overdrive.com/classic/ErrorCapture.asp? Number=201 & Message=Unable+to+perform+the+requested+query+on+the+database. & Descrip=[Microsoft][ODBC+SQL+Server+Driver][SQL+Server]Incorrect+syntax+near+the+keyword+%27WITH%27.%3Cbr%3EEXEC+sp_MediaLocatorDLRSearchQuery+%27CreatorID%27%2C+N%27985E2335-DB21-46E2-B28A-481D752ACA86%27%2C+%27coinSortTitle%27%2C+%27+ASC%27%2C+1%2C+10 "[Microsoft][ODBC+SQL+Server+Driver][SQL+Server]Incorrect+syntax+near+the+keyword+'WITH'.<br>EXEC+sp_MediaLocatorDLRSearchQuery+'CreatorID'%2C+N'985E2335-DB21-46E2-B28A-481D752ACA86'%2C+'coinSortTitle'%2C+'+ASC'%2C+1%2C+10" "[Microsoft][ODBC+SQL+Server+Driver][SQL+Server]Incorrect+syntax+near+the+keyword+'WITH'.<br>EXEC+sp_MediaLocatorDLRSearchQuery+'CreatorID', +N'985E2335-DB21-46E2-B28A-481D752ACA86', +'coinSortTitle', +'+ASC', +1, +10" http://search.overdrive.com/classic/ErrorCapture.asp? Number=201 & Message=Unable+to+perform+the+requested+query+on+the+database.%3Cbr%3EEXEC+sp_MediaLocatorDLRSearchQuery+%27Title%27%2C+N%27iron+druid%27%2C+%27coinSortTitle%27%2C+%27+ASC%27%2C+2%2C+10 & Descrip=[Microsoft][ODBC+SQL+Server+Driver]Timeout+expired */ const QuickSearchsDays = 25; const QuickSearchsCount = 25; const DaysOfInterest = [3,7,14,30,45,60,90,180,365,3653]; const DefaultCheckoutPeriod = 7; const AttributeNamePrefix = "u"; const XPathProfilingLevel = 0; //I thought they had finally axed this, but it now works again. They are crazy for having unsanitized SQL inputs. //I didn't come up with this hack, I just found it deployed in the wild, on production websites. //I wish they would kill this and implement it in a safe way. //const evilFormatDivider = ") OR (LCPU.lftyFormatTypeID = "; //Nope looks like they have fixed it now! Yeah =^_^= function joinFormats(array) { return array.join(","); } function splitFormats(str) { return (str.split(/,\s*/)); } if(!isInAutoPagerHiddenbrowser || isInAutoPagerHiddenbrowser()) throw "done"; var location; var server; try{ location = document && document.location; server = location && location.host; } catch(e){ server = document.URL && (location = url2location(document.URL)) && location.host; log(location, e); } if(!server) throw "done"; //GM_log(location); var page; var lang; const find_id = /^[\S\s]*?([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})[\S\s]*$/; const AThe = /^(["']?)((?:[Tt]he|[Aa]n?)\s+)?(.+?)(?:\1|)$/; function processForSort(a){ return String.replace(a, AThe, "$3"); } //http://chesterfield.lib.overdrive.com/ContentDetails.htm?ID={214A38E5-37B3-461C-9AD5-EB35F8E70AC5} //A book without an author! //http://search.overdrive.com/classic/TitleInfo.asp?TitleReserveID=1B45B393-6D40-4B76-A2F8-7CB9A9A6ACE7&TitleFormatID=420 //{ Comments, notes and the such //http://contentreserve.com/TitleInfo.asp?ID={28DC2831-AC85-4473-B63A-740393596477}&Format=50 //http://www.booksamillion.com/p/Write-Your-Life/Lawrence-Block/Q699912686?id=5944404734731 //http://covers1.booksamillion.com/covers2/ebook/ImageType-100/0293-1/%7B28DC2831-AC85-4473-B63A-740393596477%7DImg100_b.jpg //http://freelibrary.lib.overdrive.com/BANGSearch.dll?Type=Format&ID=25,425 //http://freelibrary.lib.overdrive.com/BANGSearch.dll?Type=Publisher&ID=1383&PerPage=25 //Good test page //http://freelibrary.lib.overdrive.com/ContentDetails.htm?ID=A4519EDB-6DC5-4948-A0F2-3551BED4CF10 //http://audiobookstand.ret.overdrive.com/ //http://contentreserve.com/GetImage.asp?Image=0857-1\\{90613ABF-C565-4CD2-BA82-8C0799F80EB9}Img200.gif&Default=_Pub_NoImage_Img200.gif //http://contentreserve.com/EnhancedDetails.asp?ID={28DC2831-AC85-4473-B63A-740393596477}&Format=50 //Chseter doesn't have counts in the waitinglist!!! //It is unavoidable, I will need to write profiles for different sites. Eg search @ erl.lib.overdrive.com //https://marketplace.overdrive.com/TitleDetails?crid=3b8441f8-6091-42d9-8774-f24d7ea0e4c3 //V1 retailers!!! <-- uses classic v1 website!!! Surprisingly some of the crazy stuff still works. //http://ebooks.carinapress.com/ContentDetails.htm?ID=E5C99018-B62C-4070-BC03-5F383BAD809C //http://audiobookstand.ret.overdrive.com/ContentDetails.htm?ID={6FF7699B-72AA-444A-B7D0-E0CE67DE4A39} //http://www.audiobookstanddl.com/ContentDetails.htm?ID={6FF7699B-72AA-444A-B7D0-E0CE67DE4A39} //ContentDetails.htm?ID={0889499B-ECE8-41DF-A725-258B93037B2C} //http://search.overdrive.com/TitleInfo.asp?TitleReserveID={0889499B-ECE8-41DF-A725-258B93037B2C}&TitleFormatID=25 //http://search.overdrive.com/ti/0889499B-ECE8-41DF-A725-258B93037B2C //http://www.contentreserve.com/TitleInfo.asp?ID={0889499B-ECE8-41DF-A725-258B93037B2C}&Format=25 //TitleFormatID // 25 - OverDrive WMA Audiobook // 30 - OverDrive Music // 35 - OverDrive Video // 40 - OverDrive Video (mobile) // 50 - Adobe PDF eBook // 410 - Adobe EPUB eBook // 425 - OverDrive MP3 Audiobook // 900 - Mobipocket eBook //ISBN search should allow for finding multi-format books //EnhancedDetails will also detail other available formats - but only if they are enhanced :-/ //but it is a quick and dirty way of getting the book title. //Easy way to find multi-format books. //http://search.overdrive.com/SearchResults.asp?ReserveID={0889499B-ECE8-41DF-A725-258B93037B2C} //http://search.overdrive.com/retail/SearchResults.asp?ReserveID={0889499B-ECE8-41DF-A725-258B93037B2C} //Does not catch editions with other ids. //Note: retail and library db not identical! //SMS related stuff //FindCarrier.htm //TextRates.htm //Libraries.htm //ODAdvantage.htm //Software.htm //ContentDetails-GeoRights.htm?ID={0FB2A31B-FF83-4927-80B0-6BBEE4A9214F} //ContentDetails-StreetDate.htm?ID={0889499B-ECE8-41DF-A725-258B93037B2C} //ContentDetails-ReadExcerpt.htm?ID={0FB2A31B-FF83-4927-80B0-6BBEE4A9214F} //EnhancedDetails.htm?ID={0889499B-ECE8-41DF-A725-258B93037B2C} //BANGSearch.dll?Type=Advanced&UserRating=1&URL=SearchResultsUserRank.htm //BANGSearch.dll?Type=Series&ID={AA2583D2-B2A5-4DD6-A1C1-CE5487289AB0}&SortBy=CollDate //BANGSearch.dll?Type=Creator&ID={F6F6EAAF-46D0-4F54-BECF-51F5D2B88104} //BANGSearch.dll?Type=Subject&ID=622 //BANGSearch.dll?Type=Awards&ID=68 //BANGSearch.dll?Type=Subject&ID=1069&Format=25,30,425&SortBy=CollDate //BANGSearch.dll?Type=Series&ID={B371EB3F-4915-4243-86A3-62560D75AA6E}&SortBy=CollDate //BANGSearch.dll?Type=Format&ID=25,30,425&SortBy=CollDate //BANGSearch.dll?Type=Format&ID=425&url=SearchResultsmp3.htm //BANGSearch.dll?Type=Seek&Letter=M&SearchID=30508654&SortBy=title&SortOrder=ASC //BANGSearch.dll?searchresult=3709541s&more=1&PerPage=25 //NewMP3Additions.htm //BrowseeBookFiction.htm //BrowseeBookNonfiction.htm //BrowseFiction.htm //BrowseNonfiction.htm //BrowseYouth.htm //BrowseVideo.htm //xPod.htm //MoreRecommend.htm?ID=85E2063C-E1C6-408D-9024-544E6CD1AE27 //BANGSearch.dll?Type=FreeForm //POST:SearchResult=30508654&FreeFormFields=title%2Ccreator%2Ckeyword&FreeFormCriteria=&Availcopies=%3E*0 //BANGSearch.dll?Type=FullText&SortBy=relevancy //POST:FullTextField=All&FullTextCriteria=test&Format=all&availabilityoptions=more&x=4&y=11 //http://search.overdrive.com/classic/TitleInfo.asp?TitleReserveID=6F1EDE5C-218F-41D2-A222-E7BF54444A8A&TitleFormatID=425 //http://search.overdrive.com/classic/TitleInfo.asp?TitleReserveID=FDB2BAB0-751C-4424-949F-B8684F72D12F&TitleFormatID=425 //http://search.overdrive.com/EnhancedDetails.aspx?ID=FDB2BAB0-751C-4424-949F-B8684F72D12F&Format=35 //search.overdrive.com //SearchResults.aspx?Series="Bloodsucking Fiends" //Search Backend //http://search.overdrive.com/SearchResults.svc/GetResults?sEcho=1&iColumns=18&sColumns=ReserveID%2CTitle%2CSubtitle%2CEdition%2CSeries%2CPublisher%2CFormat%2CFormatID%2CCreators%2CThumbImage%2CShortDescription%2CWorldCatLink%2CExcerptLink%2CCreatorFile%2CSortTitle%2CAvailableToLibrary%2CAvailableToRetailer%2CRelevancyRank&iDisplayStart=0&iDisplayLength=64&sSearch=&bEscapeRegex=true&iSortingCols=1&iSortCol_0=17&sSortDir_0=asc&dCurrentDateTime=Fri+May+28+2010+01%3A18%3A25+GMT-0400+(Eastern+Daylight+Time) //http://search.overdrive.com/SearchResults.aspx?kw=%22Indigo%20Court%22&sortBy=RelevancyRank&searchCol=Series&ascending=True&pageSize=50&page=1&showEbooks=False&showAudiobooks=True&showMusic=False&showVideo=False //http://search.overdrive.com/SearchResults/ReserveID/%7BF750E2CA-2D38-4CC8-A74D-0517751CD2C9%7D //http://search.overdrive.com/TitleInfo.aspx/GetCountryRegionListByTitle //Post Body: escape(JSON.stringify({"szReserveID":"b94dfea1-45bf-4800-82f8-c75b2cbb6a55","nFormatID":25})) //http://search.overdrive.com/Layout/LoadCountriesLib?X-Requested-With=XMLHttpRequest&hidCountryValLib=1 //http://search.overdrive.com/Layout/LoadStates?countryCode=us&searchType=LoadCountriesLib&continentID=1 //http://search.overdrive.com/SearchResults/SearchLibrariesByState?state=NJ&libcountry=us //http://search.overdrive.com/TitleInfo.aspx/SearchForALibrary //escape(JSON.stringify({"szReserveID":"b94dfea1-45bf-4800-82f8-c75b2cbb6a55","nFormatID":25,"szType":"Region","szValues":"Florida|FL|United States|United States"})) //http://us.newsletter.overdrive.com/4ADD9EB8-F2B3-40EE-B0DC-255DD65AF176/1/124/en/ContentDetails.htm?ID={27E34716-21DC-49E3-A957-A0CE370C8AC1} //http://digital.minlib.net/Recommend.htm?ID={DDF7AEF4-751D-4301-A612-C7B33A0D51D5} //http://www.booksonboard.com/sellers/halfsteps/ir.php?CRID=12B1D39F-1A1E-4084-8D6B-82EF73EA1B29-25&sellerID=0996-0001 //http://images.contentreserve.com/CreatorPhotos/1841-1/%7BFB19E3B8-ABE5-49DA-B858-419BB5EE1522%7DCreator.jpg //http://overdrive.lib.uts.edu.au/ContentDetails.htm?id=3F9AB83A-A90C-48CE-9FFD-58BF528E68F7 //http://overdrive.lib.uts.edu.au/BANGSearch.dll?Type=Creator&ID=FB19E3B8-ABE5-49DA-B858-419BB5EE1522 <- this works still!!! //http://overdrive.lib.uts.edu.au/BANGSearch.dll?Type=Creator&ID=176441 <- this works too //Unfortunately there seems to be no way to match new author id's to UUIDs, atleast not that I have found yet. //Edition: Unabridged Selections //TODO use the new search, it's not as screwed up as the classic search. //TODO parse ContentDetails.htm with titleinfo.asp functions. //TODO Global Wishlist //NOTE: CR, Classic & Retail have seperate pages for enhancements, Modern doesn't contain links to them but can display them. //} if(DaysOfInterest.indexOf(QuickSearchsDays) == -1) { DaysOfInterest.push(QuickSearchsDays); DaysOfInterest.sort(function(a, b){return a - b;}); } Object.freeze(DaysOfInterest); GM_addStyle = function(css, doc) { doc = doc || document; var head = doc.getElementsByTagName("head")[0]; var style; if (head) { style = doc.createElement("style"); style.textContent = css; style.type = "text/css"; head.appendChild(style); } return style; }; String.prototype.endsWith = function(str){ var p = this.length - str.length; return (str.length === 0) || (p >= 0) && (this.indexOf(str, p) == p); }; String.prototype.startsWith = function(str){ return !(str.length && this.lastIndexOf(str, 0)); }; function CollectionFiltering(target, options, objs){ options.series = options.series || (options.series === undefined);//HACK too lazy to update call sites. let root = (options.root?options.root+" ":""); if(!target){log("CollectionFiltering", arguments); return;} // {ready:true, filter:true, fields:{Series:"series", Creators:"creator"}} function quickStringSort(a,b){ return (a > b) - (a < b); } function FieldRunner(obj){ //TODO proxy sortValues so they are easier to deal with. for(let field of fields) { let Value = GetFieldValue(obj, field.fields); let value = Value; //if(value === undefined) // value = (value = GetFieldValue(obj, field.fields.map(function(f){return f+"s"}))) && value[0]; if(Value == null) Value = value = ""; else if(typeof(Value) === "string") value = (Value = Value.trim()).toLowerCase(); let sortValue = processForSort(value); var name = field.name; var pname = field.pname; if(field.known.hasOwnProperty(sortValue)) field.known[sortValue].counter++; else { let button; // try { button = smart_buttons_column(field.container, [value?//{{ {id:name+"-"+sortValue, text:field.serialize(Value), selector: root+".item["+pname+"=\""+sortValue.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")+"\"]", /*display:"table-row",*/ sort:sortValue}: {id:name+"~none", text:"<None>", selector:root+".item:not(["+pname+"])", /*display:"table-row",*/ sort:sortValue}, ])[0];//} // } catch(e){} if(button) { field.container.setAttribute("count", ++field.counter); let k = quickSetValueProperty(field.known, sortValue, { counter: 1, node: button.span, button: button, sort: value, }); let names = field.container.children; for(var i = 1, before, node; (reverse * typesort(value, before = field.known[(node = names[i]).getAttribute("sort", "")].sort)) > 0; i++); if(before != value) insertBefore(remove(k.node), node); } } if(value) for each(var n in obj.pieces) n.setAttribute(pname, sortValue); } } function delayedTask(func, delayDefault, name){ let that = this; let timeout = null; this.callback = func; this.delayDefault = delayDefault; Object.defineProperty(this, "queued", { enumerable: true, configurable: false, get:function(){ return timeout !== null; }, }); this.run = function(){ //log("running - " + name); let m = that.callback.apply(that, arguments); that.clear(); return m; }; this.requeue = function(delay){ if(delay === undefined) delay = delayDefault ; //log("requeuing - " + name); if(timeout) that.clear(); if(delay > 0) timeout = window.setTimeout(that.run, delay); else that.run(); return that; }; this.queue = function(delay){ if(delay === undefined) delay = delayDefault ; //log("queuing - " + name); if(delay > 0) { if(!timeout) timeout = window.setTimeout(that.run, delay); } else that.run(); return that; }; this.clear = function(){ //log("clearing - " + name); if(timeout) window.clearTimeout(timeout); timeout = null; return that; }; } function Field(tname, names, serialize){ this.tname = tname; this.fields = [].concat(names); this.pname = AttributeNamePrefix + (this.name = this.fields[0]); this.known = {}; this.counter = 0; this.serialize = serialize || NoOp; } function addItem(obj){ let formats = (Array.isArray(obj.formats)?obj.formats:[f for (f in obj.formats)]).map(Number); obj.installMetadataForFiltering(); if(!obj.has("pos")) { var used = uniques[obj.id] || (uniques[obj.id] = []); if(!used.some(function(f){return formats.indexOf(f) != -1;})) { obj.pos = items.length; // originals.push(obj); for each(let num in formats) { used.push(num); if(known_formats.indexOf(num) == -1){ known_formats.push(num); let format = Formats.NumberLookup(num); let type = format.type; let type_obj = Formats.Classes[type]; let div = format_containers[type] || makeFormatContainer(type); smart_buttons_column(div, [{ id:num, text:format.title.replace(" "+type_obj.title,""), selector:root+".item-format-"+num, }]); } } items.push(SmartSeries.add(obj)); } else {//duplicate! for each(var n in obj.pieces) n.classList.add("item-duplicate"); log("duplicate", obj); } } let sample = obj.sample?true:false; for each(let n in obj.pieces){ n.setAttribute("sample", sample); } let formatGroup = (obj.formatGroup || "Unknown").replace(/Online /g,""); for each(var n in obj.pieces){ n.classList.add("format-"+formatGroup); } //not sure if I want to keep this functionality if(countries && obj.countries) { for each(var c in obj.countries) if(countries.list.indexOf(c) == -1) { countries.list.push(c); countries.add({text:(CountryCodes.hasOwnProperty(c) && CountryCodes[c]) || c, value:root+".item[countries]:not([countries~="+c.toLowerCase()+"]), "+root+"span.provider[country]:not([country~="+c+"]) "}); } } return obj; } function typesort(a, b){ if((a === b)) return 0; if((a == null)) return ((a == b) || (b == new b.constructor()))?0:1; if((b == null)) return (a == new a.constructor())?0:-1; switch((typeof(a))){ case "number": return a - b; case "string": return quickStringSort(processForSort(a), processForSort(b)); case "object": if(isArray(a)){ if(a == b) return 0; for(var i = 0, j = Math.min(a.length, b.length), k; i < j; i++) if((k = typesort(a[i], b[i]))) return k; return a.length - b.length; } } return -1; } function sort(){ //log("updating"); let by = sortOrder.value.split(","); items = items.sort(function(a, b){ for each(var name in by) { let d = (reverse * typesort(a[name], b[name])); if(d) return d; } return 0; }); //log(items.map(function(f){return [(a+":"+((f[a]==null)?"":f[a])) for each(a in by)]})); sorted(); // log("updated"); // log(items) } function sorted(){ for each(let item in items) for each(let piece in item.pieces) if(piece.parentNode)//some were wrapped in comments but were added so they could be parsed, don't insert these. insertBefore(piece, bottom); for each(let table in pagingTrash) remove(table); pagingTrash = []; } function SmartItemProxy(){ this.objs = []; this.proxies = [];} function makeFormatContainer(type){ let div = format_containers[type] = document.createElement("div"); div.id = "formats-"+type; let type_obj = Formats.Classes[type]; smart_buttons_column(div, [{id:type, title:type_obj.title, flip:/**/"cell"/*/type_obj.formats*/, selector:root+".format-"+type_obj.title}]); format_container.appendChild(div);//this is the wrong solution! return div; } let buttons = [objMerge([], {id:"main-container"}), objMerge([], {id:"format-container", style:options.formatGroup?"display:none;":""})]; this.target = target; SmartItemProxy.prototype = { add:function(obj){//add an object to the group and returns a proxy for the obj //While it would make sense to only update the parts of the cache that are affected by adding a new obj //it's simpler to just nuke the cache and have it recalc during the next request delete this.cache; //this is where we actually add the object to the group. this.objs.push(obj); //Now we build the proxy let proxy = Object.create(obj, {//this creates an object that inherits the properties from obj but has getters that override obj. group:{value:this, enumerable:true},//makes it all self contained. "series":{get: function(){ return this.group.get("series"); }, enumerable:true}, "Series":{get: function(){ return this.group.get("Series"); }, enumerable:true}, "series-position":{get: function(){ return this.group.get("series-position"); }, enumerable:true}, // "contentUpdate":{get: FieldRunner, enumerable:true }, }) this.proxies.push(proxy); this.proxies.forEach(FieldRunner); return proxy; }, "get":function(field) { //skip the cache if there is only a single object in the group if(this.objs.length == 1) return this.objs[0][field]; let cache = this.cache || (this.cache = {}); let value = cache[field]; let before = value; if(value === undefined) { value = this.calculate(field); if(value !== undefined) cache[field] = value; // log(field, value, cache[field] /*, this, this.objs.map(function(obj){return obj[field];})*/); } // let post = this.calculate(field); // log(this.objs[0].title, field, before, (value == before) || value, (post == value) || post); return value; }, calculate:function(field){ let count = {}; let map = {}; let max = 0; let maxs = []; for each(let obj in this.objs) { let v = obj[field]; if(v !== undefined) { let c = (count[v] || 0) + 1; if(c == 1) map[v] = v; count[v] = c; if(c > max) max = c; } } let order = Object.getOwnPropertyNames(count).sort(function(a,b){ return typesort(count[b], count[a]);}).map(function(v){return map[v]}); return order.filter(function(f){return f;})[0] || order[0]; }, }; //TODO //The goal of SmartSeries is to cope with titles that have not been completely tagged (lack series information) //We combine the metadata from several titles so that they can be grouped together. //This is largely transparent to the insides of this function and operate as a proxy layer applied during this.add let SmartSeries = { add:function(obj){ let name = [obj.title,obj.creator].join("\x00"); return (this[name] || (this[name] = new SmartItemProxy())).add(obj); }, }; // let originals = []; let pagingTrash = []; let uniques = {}; let itemClass = "item" let known_formats = []; let format_containers = {}; let format_container; let items = []; let reverse = 1; let sortOrder; let bottom; let countries; let filterNode; let filter; let counter; this.add = function(objs, table){ //log("add", arguments, isArray(objs)); if(isArray(objs)) { let out = []; for(let i = 0; i < objs.length; i++) { let obj = objs[i]; out.push(addItem(obj)); } if(options.paging){ if(filterNode && filterNode.value){ if(!filter.queued) filter.run(table); } if(counter) counter.requeue(); if(table) pagingTrash.push(table); if(sortOrder.value != "pos" || reverse != 1) sortTimer.requeue(); else if(bottom) sorted(); // sortTimer.run(); } return out; } } // let sortTimer = new delayedTask(sort, 75, "sort"); if(options.countNodes && options.countNodes.length) counter = new delayedTask(function(){ let j = items.length + (options.countNodes.start - 1); for(let i = 0; i < options.countNodes.length; i++) options.countNodes[i].nodeValue = j; }, 0, "count"); let temp; let fields = [//{ new Field("Creators","creator"), new Field("Title", "title"), ];//} if(options.series){ fields.splice(0, 0, new Field("Series", ["Series"])); fields.splice(2, 0, new Field("Book", "series-position", function(r){return r.length>1?r[0]+"-"+r[r.length - 1]:r[0];})); } if(options.formatGroup) { buttons[0].push( {title:"Format", toggle:"qFormat"}, {id:"qFormat", important:true, dropdown: [ {text:"All"}, {text:"Audiobooks", selector:[/*"Audiobook",*/"eBook","Unknown","Video","Unknown"].map((v)=> root+".format-"+v).join(", ")}, {text:"eBooks", selector:["Audiobook",/*"eBook",*/"Video","Unknown"].map((v)=> root+".format-"+v).join(", ")}, {text:"Videos", selector:["Audiobook","eBook",/*"Video",*/"Unknown"].map((v)=> root+".format-"+v).join(", ")}, {text:"Unknown", selector:["Audiobook","eBook","Video"/*,"Unknown"*/].map((v)=> root+".format-"+v).join(", ")}, ], } ); } if(options.ready) { buttons[0].push( {title:"Status", toggle:"ready"}, {id:"ready", important:true, dropdown: [ {text:"All"}, {text:"Available", selector:root+".item-unavailable"}, {text:"Ready", selector:root+".item-wait, "+root+".item-unavailable"}, {text:"Hold", selector:root+".item-ready, "+root+".item-unavailable"}, {text:"Unavailable", selector:root+".item-wait, "+root+".item-ready"}, ], } ); } if(options.availability) { buttons[0].push( {title:"Availability", toggle:"availability"}, {id:"availability", important:true, dropdown: [ {text:"All"}, {text:"Library", selector:root+".library-unavailable"}, {text:"Store", selector:root+".store-unavailable"}, {text:"WorldCat", selector:root+".worldcat-unavailable"}, {text:"Unavailable", selector:root+".library-available, "+root+".store-available"}, ]} ); } if(options.provided) { buttons[0].push( {title:"Availability", toggle:"provided"}, {id:"provided", important:true, dropdown: [ {text:"All"}, {text:"Available", selector:root+".item-unavailable, "+root+".item-inactive"}, {text:"Available & Inactive", selector:root+".item-unavailable"}, {text:"Inactive", selector:root+".item-unavailable, "+root+".item-active"}, {text:"Inactive & Unavailable", selector:root+".item-active"}, {text:"Unavailable", selector:root+".item-active, "+root+".item-inactive"}, ]} ); } if(options.providers) { buttons[0].push( {title:"Providers", toggle:"providers"}, {id:"providers", important:true, dropdown: [ {text:"Show All"}, {text:"Available & Inactive", selector:root+"span.subscription-none"}, {text:"Available", selector:root+"span.subscription-none, "+root+"span.subscription-inactive"}, {text:"Inactive", selector:root+"span.subscription-none, "+root+"span.subscription-active"}, {text:"Hide All", selector:root+"span.subscription-none, "+root+"span.subscription-active, "+root+"span.subscription-inactive, "+root+"[parsed='Available at']"}, // {text:"Unavailable", selector:root+".item-available, "+root+".item-inactive"}, ]} ); } if(options.abridged) { buttons[0].push( {title:"Abridgment", toggle:"abridgment"}, {important:true, dropdown: [{text:"All"}, {text:"Unabridged", selector:root+".item-abridged"}, {text:"Abridged", selector:root+".item-unabridged"}], id:"abridgment"} ); } if(options.filter) { buttons[0].push( {title:"Hide These", alt:"Items that match the regular expression entered are hidden from view"}, objMerge(filterNode = document.createElement("input"), {id:"filter", type:"text", title:"Items that match the regular expression entered are hidden from view"}), document.createElement("br") ); } if(options.countries) { buttons[0].push( {title:"Countries", toggle:"countries"}, countries = {id:"countries", important:true, dropdown: [ {text:"All", value:""}, // {text:"US", value:".item[countries]:not([countries~=us]), span.provider[country]:not([country~=US]) "}, ], list:[""], } ); } if(options.sample) { buttons[0].push( {title:"Sample", toggle:"samples"}, {id:"samples", important:true, dropdown: [ {text:"All", value:""}, {text:"With", selector:root+"[sample='false']"}, {text:"Without", selector:root+"[sample='true']"}, ], } ); } //{//generate everything but the containers buttons[0].push(//{ {title:"Sort", toggle:"sort"}, sortOrder = {id:"sort", dropdown: [ {text:"Default", value:"pos"}, {text:"Series", value:"Series,series-position,title,pos", skip:!options.series}, {text:"Title", value:"title,creator,pos" }, {text:"Author", value:"creator,title,pos" }, ], event: sortTimer.requeue, }, {text:"Descending/Ascending", id:"sort", event:function(e){ reverse = -reverse; for(let field of fields) for each(n in [n for each(n in field.known)].sort(function(a, b){return reverse * typesort(a.sort, b.sort);})) field.container.appendChild(remove(n.node)); sortTimer.run(); } } );//} for(var i = 0; i < fields.length; i++){ let name = fields[i].tname fields[name] = fields[i];//makes code down compatible buttons.push(objMerge([{id:name, title:name, flip:"cell"}], {"max-height":"17em", id:name, backer:name, className:"button-category"})); } //} let tr = target//{ .appendChild(objMerge(document.createElement("table"), {id:"selectors"})) .appendChild(document.createElement("tbody")) .appendChild(document.createElement("tr"));//} GM_addStyle([//{ "table#selectors {width:100%; clear:both;}", "table#selectors td { vertical-align:top; padding: 1px; }", "table#selectors td div > .boxlabel { white-space:nowrap; }", "table#selectors td div > .boxlabel > label { white-space:normal; display: inline; }", "table#selectors td div > label.title { text-decoration:underline; font-weight:bold; }", "table#selectors td div .toggle, table#selectors td div .flip { cursor:pointer; -moz-user-select: none;}", "table#selectors td div > label { vertical-align: 2px;}", "table#selectors td div.limit { overflow-y: auto; margin-right:1.5em; width:100%;}", "table#selectors div.button-category[count='1'] { display:none; }", ".item.filtered {display:none;}", "td.FormatTitle {max-width: 1em; overflow:visible; white-space:pre;}", "div.FormatTitle {-moz-transform: rotate(90deg); overflow: visible;}", ".FormatTitle input {margin-left: -1px;}", ".FormatTitle label {width: auto;}", ".Hidden, .HiddenSearchable {display:none!important;}", // "#Book.limit div { max-width:10em; overflow:hidden; text-overflow:ellipsis; }", "table#selectors input[type='text'] {display: inline-block!important; padding:1px; margin:0; height:1.5em;}", "table#selectors select { padding:1px; }", ].join("\n"));//} for(i = 0; i < buttons.length; i++) { var k = buttons[i]; let cell = document.createElement("td"); tr.appendChild(cell); let div = document.createElement("div"); cell.appendChild(div); if(k["max-height"]) { div.className = "limit" div.style.maxHeight = k["max-height"]; } if(k.className && k.className.length != 0) for(let c of [].concat(k.className)) div.classList.add(c); if(k.id) div.id = k.id; if(k.style) div.style = k.style; smart_buttons_column(div, k); k.container = div; if(k.backer) fields[k.backer].container = div; } if(sortOrder) sortOrder = sortOrder.select || sortOrder; format_container = format_container || buttons[1].container; if(options.filter) { let search = compileXPath(".//*["+["@parsed"].concat([//{ 'Available at',//Doesn't make sense to hide an item because it is available at a particular library and we have better filter tools available 'ISBN',//Makes series searches more complicated ].map(function(n){return "@parsed!="+SmartQuotes(n);})).join(" and ")+"]");//} filter = new delayedTask(function(block){ block = block || bottom; let exp = new RegExp(filterNode.value); let uniques = {}; $Z(compileXPath("descendant-or-self::*[contains(concat(' ', @class, ' '), ' item ')]"), function(node){ let u = node.getAttribute("unique"); let nodes = uniques[u]; if(nodes == null) nodes = uniques[u] = []; nodes.push(node); node.classList.remove("filtered"); }, block && block.parentNode); //log(uniques) if(filterNode.value) for each(var ar in uniques) if(ar.reduce(function(a,b){return a.concat($W(search, b));}, []).some(function(n) {return n.textContent.match(exp);})) for each(var node in ar) node.classList.add("filtered"); }, 2000, "filter"); let filter_update = function(evt){ switch(evt && evt.keyCode) { case 27://escape filterNode.value = ""; case 0xa: case 0xd://enter filter.run(); return; default: } filter.requeue(); } addEvent(filterNode, "keyup", filter_update); addEvent(filterNode, "change", filter_update); } if(objs) { //add these objects this.add(objs); } log(items); // log(SmartSeries); if(items.length) { bottom = insertAfter(document.createComment("Anything after this is unknown to the sorting code."), items.slice(-1)[0].pieces.slice(-1)[0]); } //log(fields); } //{ ProviderBase function ProviderBase(){} ProviderBase.prototype = { linkify: function(obj){ var sp = this; var text, node; var a; var cleanups = [] if((a = sp.createLink(obj.titleLinkType, obj)) && (text = obj["subtitle-positioning"])//{ subtitle-positioning && (node = obj.nodes["Subtitle-value"]) && (node = findTextElementContaining(text, node, "not(ancestor-or-self::a)")) && (node = SplitTextNode(node, text))) { cleanups.push(insertAsParent(a.cloneNode(true), node)); }//} if((a = sp.createLink("series", obj)) && (text = obj["subtitle-series"])//{ subtitle && (node = obj.nodes["Subtitle-value"]) && (node = findTextElementContaining(text, node, "not(ancestor-or-self::a)")) && (node = SplitTextNode(node, text, obj["subtitle-Series"]))) { cleanups.push(insertAsParent(a.cloneNode(true), node)); }//} if(a && (text = obj["title-series"])//{ title-series && (node = obj.nodes["Title-value"]) && (node = findTextElementContaining(text, node, "not(ancestor-or-self::a)")) && (node = SplitTextNode(node, text))) { cleanups.push(insertAsParent(a.cloneNode(true), node)); }//} if(a && (text = obj.series)//{ series && (node = obj.nodes["Series-value"]) && (node = findTextElementContaining(text, node, "not(ancestor-or-self::a)")) && (node = SplitTextNode(node, text))) { cleanups.push(insertAsParent(a.cloneNode(true), node)); }//} if(obj.creator//{ //creator - should be doing this with creatorObjs instead. // {name:creator, role:role, id:undefined, number:undefined, link:undefined, text:value} && (node = obj.nodes["Creators-value"] || obj.nodes["Creator-value"] || obj.nodes["Author-value"]) && (node = findTextElementContaining(obj.creator, node, "not(ancestor-or-self::a)")) && (a = sp.createLink("creator", obj)) && (node = SplitTextNode(node, obj.creator))) { cleanups.push(insertAsParent(a, node)); }//} if((a = sp.createLink(obj.titleLinkType, obj)) && obj.title//{ title && (node = obj.nodes["Title-value"]) && (node = findTextElementContaining(obj.title, node, "not(ancestor-or-self::a)")) && (node = SplitTextNode(node, obj.title))) { cleanups.push(insertAsParent(a.cloneNode(true), node)); }//} if("convertLink" in sp){ for each(var a in cleanups) sp.convertLink(a); } if("suggestions" in obj){ for each(let s in obj.suggestions) this.linkify(s); } if("supplements" in obj){ for each(let s in obj.supplements) this.linkify(s); } if(obj.publisher == "Project Gutenberg"){ let span = obj.nodes["Publisher-node"].appendChild(document.createElement("span")) span.className = "sup"; let a = span.appendChild(document.createElement("a")); a.href = "http://www.gutenberg.org/ebooks/search.html/?"+obj2post(obj, {query:"title"}); a.appendChild(document.createTextElement("T")) a.title = "Search Project Gutenberg for title." } if(obj.format == 420 && obj.ASIN){//kindle let span = obj.nodes["ASIN-node"].appendChild(document.createElement("span")) span.className = "sup"; let a = span.appendChild(document.createElement("a")); a.href = "http://www.amazon.com/"+obj.title+"/dp/"+obj.ASIN+"/"; a.appendChild(document.createTextNode("US")) a.title = "Search Amazon for this title." } if(obj.ISBN){ let span = (obj.nodes["ISBN-node"] || obj.nodes["Lib. ISBN-node"] || obj.nodes["Ret. ISBN-node"]).appendChild(document.createElement("span")) span.className = "sup"; let a = span.appendChild(document.createElement("a")); a.href = "http://www.worldcat.org/search?q="+obj.ISBN; a.appendChild(document.createTextNode("WC")) a.title = "Search WorldCat.org for this title." } return cleanups; }, } //} //{ Item function Item(pieces){ this.unique = new Date().valueOf() + "~" + Math.random(); if(isArray(pieces)) this.addPieces(pieces); //quick solution for finding an obscure bug //this.nodes.__defineSetter__("Mobipocket eBook-node", function(y) { try{"".evaluate();}catch(e){log(e);}}); } Item.prototype = lazyProperties(installSelfDestructingGettersAndSetters({ get Series(){return GetFieldValue(this, ["series-series", "series"].concat((this.seriesFieldParts || []).map(function(s){return s+"-Series";})).concat((this.seriesFieldParts || []).map(function(s){return s+"-series";}))) || "";}, has: function(name){//ignore getters & setters! return (!this.__lookupGetter__(name)) && (name in this); }, get creator(){return (this.creatorObjs.length > 0)?this.creatorObjs[0].name:undefined;}, get creatorId(){return (this.creatorObjs.length > 0)?this.creatorObjs[0].id:undefined;}, get creatorNumber(){return (this.creatorObjs.length > 0)?this.creatorObjs[0].number:undefined;}, get creators(){return Object.freeze(this.creatorObjs.map(function(c){return c.name;}));}, get Formats(){return (Array.isArray(this.formats) && this.formats.slice()) || Object.keys(this.formats)}, get format(){return this.Formats[0];}, set format(value){ if(Array.isArray(this.formats)) { if(this.formats.indexOf(value) == -1) this.formats.unshift(value); } else throw "Item.formats is not an array so it cannot be set with the format setter"; }, isNodeParsed: function(node, name){ if(!name) return true; let flagNode = getTag(node); return ("~"+flagNode.getAttribute("parsed", "")+"~").indexOf("~"+name+"~") != -1; }, setNodeParsed: function(node, name) { if(!name) return true; let flagNode = getTag(node); let parsed = flagNode.getAttribute("parsed", ""); if(("~"+parsed+"~").indexOf("~"+name+"~") != -1) return true; flagNode.setAttribute("parsed", parsed?parsed + "~" + name:name); return false; }, /* removedNodeParsed: function(node, name) { let flagNode = $X(compileXPath("ancestor-or-self::*[1]"), node) let parsed = flagNode.getAttribute("parsed", ""); if(parsed) flagNode.setAttribute("parsed", ("~"+parsed+"~").replace("~"+name+"~", "~").slice(1,-1)); },/**/ titleLinkType:"title", textSort: function(text){ if(text == null || text == "") return text; return processForSort(text.toLowerCase()); }, seriesFieldParts: Object.freeze(["subtitle", "title", "series"]), processSubtitle:function(){ if(this.hasOwnProperty("series-position")) return true; let obj = this; this.seriesFieldParts.some(function(name) { let value = obj[name]; if(value) { let match = obj.subtitleParser.exec(value); // log(value, match); if(match) { let books = []; if(match.rns) for(var i = match.rns, j = match.rne; i <= j; books.push(i++)); else { if(match.comma) books = match.comma; if(match.single) books.push(match.single); } //I am so not handling lettered series! It's silly! Anyway, is I-V 1-5 or 9-22? //if(match.ras) for(var i = match.ras.charCodeAt(0) - 64, j = match.rae.charCodeAt(0) - 64; i <= j; books.push(i++)); if(match.title) obj[name+"-title"] = match.title; if(books.length > 0) { obj[name+"-series"] = match.series; obj[name+"-series-sort"] = obj.textSort(match.series); obj[name+"-positioning"] = match.positionText; obj["series-position"] = books; } else if(match.stories) { var str = match.stories; obj[name+"-series"] = str; obj[name+"-series-sort"] = obj.textSort(str); str = str[0].toUpperCase() + str.slice(1); obj[name+"-Series"] = str; if(match.collection){ obj[name+"-positioning"] = match.collection; obj["series-position"] = [match.collection]; } } return true; } } return false; }); return this.hasOwnProperty("series-position"); }, addPieces:function(piece, root){ let obj = this; let count = 0; if(piece == null) return count; let add = function(m){ if(!m.hasAttribute("unique")) { obj.pieces.push(m); m.setAttribute("unique", obj.unique); count++; } }; if(typeof(piece) == "string") piece = [piece]; else if(piece.ownerDocument) piece = Array.slice(arguments); if(isArray(piece)) { if(typeof(piece[0]) == "string") { let unique = "[not(@unique)]";//contains(concat('~',@unique,'~'), '~"+this.unique+"~') if(root == null || root.ownerDocument) root = [root]; let f = compileXPath(piece.map(function(f){return f + unique;}).join(" | ")); for each(var node in root) $Z(f, add, node); } else piece.forEach(add); } return count; }, serial:function(){ var mask = {pieces:null, nodes:null, titleLinkType:null, formats:{}, enhanced:{}, supplements:{}, suggestions:{}, providers:{"": {a:null}}, reviews:{}, creatorMarketing:{}, excerpts:{}, recommended:{}, provider_countries:{"":{"":{a:null}}}}; mask.suggestions[""] = mask.enhanced[""] = mask.supplements[""] = mask.formats[""] = mask;//TODO, make these independant copies mask.reviews[""] = mask.creatorMarketing[""] = mask.excerpts[""] = mask.recommended[""] = {nodes:null}; mask.creatorObjs = {"":(mask.creatorObj = {link:null, text:null,})}; for each(let a in Array.slice(arguments)) mask[a] = null; return this.clone(mask, true); }, clone:function(ignore, strip, func){ var args = arguments; var obj = cloneObj(this, ignore, null, func || function(){if(this instanceof Item) return this.clone.apply(this, args);}); if(strip) obj.__proto__ = Object.prototype;//remove all the uncool stuff return obj; }, enhance:function(){ var formats = this.formats || (this.formats = []); if(!isArray(formats)) formats = [Number(index) for(index in formats)]; /* if(this.format && formats.indexOf(this.format) == -1) formats.push(this.format); if((formats.length > 0) && !("format" in this)) this.format = formats[0]; */ let m; if(this.id && (m = this.id.match(find_id))) this.id = m[1];//fixes id matching ~_~ this.processSubtitle(); if(this.pieces.length){ //This information is not open to interpretation or in need of correction. var classes = ['item'] if(this.has("formats")) { classes = classes.concat(formats.map(function(f){return "item-format-"+f;})); for(let name in Formats.Classes) { var c = Formats.Classes[name]; for each(var format in formats) { if(c.formats.indexOf(format) > -1){ classes.push("item-type-"+name); break; } } } } if(this.status) classes.push("item-"+this.status); if(this.id) classes.push("item-"+this.id); if(this.subscription) classes.push("item-" + this.subscription); if(this.has("provider_countries")) this.countries = [c for(c in this.provider_countries)]; for each(let name in classes) for each(let node in this.pieces) node.classList.add(name); } //log([this, this.clone(), this.serial()]); if(this.title && this.title == this.series && !("series-position" in this)) this["series-position"] = [1];//if the tile and the series are exactly the same, it's likely the first book in the series return this; }, kick: function(){ let small; let full; if(this.nodes && (small = this.nodes["Short Description-node"]) && (full = this.nodes["Full Description-node"])) { let text = compileXPath(".//text()"); let s = $W(text, small, [], function(text){if(text.textContent.trim()) return text;}); let ss = s[0]; let se = s.slice(-1)[0]; let sst = ss.nodeValue.trim(); let set = se.nodeValue.trim().replace(/[.]{3,}$/, ""); let fs = findTextElementContaining(sst, full); let fe = findTextElementContaining(set, full); let fl = $W(text, full, [], function(text){if(text.textContent.match(/\S/)) return text;}).slice(-1)[0]; if(fs && fe) { let i = fs.textContent.indexOf(sst); let j = fe.textContent.indexOf(set); if(i > -1 && j > -1) { j += set.length; let shortRange = document.createRange(); shortRange.setStart(fs,i); shortRange.setEnd(fe,j); let fullRange = document.createRange(); fullRange.setStart(fe,j); fullRange.setEnd(fl, fl.textContent.length); let shortSpan = document.createElement("span") shortSpan.className = "short descriptioned"; shortSpan.title = "Text found in both the full and short descriptions." let fullSpan = document.createElement("span") fullSpan.className = "full descriptioned"; fullSpan.title = "Text found only in the full description." GM_addStyle([//{ "span.descriptioned { font-size: inherit; }", "span.short { background-color: rgba(64, 255, 192, 0.15); }", "span.full { background-color: rgba(64, 192, 255, 0.15); }", "*[parsed='Short Description'] { display:none; }", ].join("\n"));//} try{ shortRange.surroundContents(shortSpan); fullRange.surroundContents(fullSpan); } catch(e){ return; } /*/ let s = window.getSelection(); if(s.rangeCount > 0) s.removeAllRanges(); s.addRange(fullRange); /**/ } } } }, /*getValueObj: function(name, _lang){ return languages.getValueObj(name, this.pieces, _lang); }, getValue: function(name, _lang){ return languages.getValue(name, this.pieces, _lang); },*/ getAdditional: function(additional, base){ if(arguments.length == 1) base = this.pieces; else if(typeof(base) !== "object" || !base || !base.push) base = [base]; else if(base.length === 0) base.push(null); let temp; for(let name in additional) { let alt = additional[name]; if(alt.ownerDocument) this.processAttribute(name, null, alt, []); else { let altbase = null; if(alt.node) { if(!(altbase = GetFieldValue(this.nodes, alt.node))) continue;//instead of using GetFieldValue we could use objFilter and place the values into an array, but we would need to convert alt.node into an (object?) } if(alt.nodeXpath) {//can't decide if this should be in the alt.node "if" or not. let f = compileXPath(alt.nodeXpath); if(altbase && Array.isArray(altbase)) altbase = altbase.reduce(function(a, n){return $W(f, n, a);}, []); else altbase = $W(f, altbase); if(!altbase.length) continue; } if(!altbase) altbase = base; else if(!isArray(altbase)) altbase = [altbase]; let params = alt.params || []; let results = []; if(alt.self) { this.processAttribute(name, null, alt.self, params); } else if(alt.form) { let form = $X(alt.form);//FIXME! if(form) { this.nodes[name+"-form"]=form; let map = alt.map || {}; for each(var item in Array.slice(form.elements)) if(item.name) this.processAttribute(map.hasOwnProperty(item.name)?map[item.name]:item.name, null, item, params); this.processAttribute(name, null, form, params); //this.nodes[item.name.toLowerCase()+"-value"] = item; //this.processAttribute(name, null, alt.self, params); } } else if(alt.script) { let f = compileXPath("descendant-or-self::script[contains(text(), 'var "+alt.script+" = ') and not(contains(concat('~', @parsed, '~'), '~"+name+"~'))]"); for each(var root in altbase) { let script = $X(f, root); if(script && !this.setNodeParsed(script, name)){ this.nodes[name + "-script"] = script; var m = script.innerHTML.match(new RegExp("var "+ alt.script +" = ("+(alt.match || "\"([^\n]*)\"|[^\n]*")+");")); var value; try {//this code is nessary because some times the strings are invalid (haven't had their quotes escaped!). if(m[1] == "\"\"" || m[1] == "''") value = ""; else value = eval(m[1], {});//eval it on an empty obj. } catch(e) { value = m[2]; } if(value !== undefined) { // try{ if(("field" in alt) && (("testNull" in alt)?(alt.testNull?value != null:value):true)) this[alt.field] = value; if(("number" in alt) && (("testNull" in alt)?(alt.testNull?value != null:value):true)) this[alt.number] = Number(value); // } catch(e){log(e, alt, name, value, script);} if("parse" in alt)//function(vname, value, obj, node, name, alt){obj[name] = Number(value);}, alt.parse.apply(alt, [alt.script, value, this, script, name, alt].concat(params)); } break; } } } else if((alt.field) && (alt.value)) { this.nodes[name+"-count"] = 0; let pairs = this.nodes[name+"-pairs"] = []; let f = compileXPath(alt.field); let data = alt.value; let end = alt.hasOwnProperty("end")?alt.end || "":":"; let obj = this; let fp = [obj, name, alt, results].concat(params); if(typeof(data) === "string") data = compileXPath(data); let func = function(r){ let title = r.textContent.trim(); let success = !end.length || (title.slice(-end.length) == end); if(success) { let field = title.replace(/[\s]+/g, ' ');//bloody ! if(end.length) field = field.slice(0, -end.length); let dataNode; if(typeof(data) === "function")//It's a function! Lets call it dataNode = data.apply(obj, Array.slice(arguments).concat(fp)); else//i guess it's an xpath expression? dataNode = $X(data, r.ownerDocument && r); if(!dataNode) { pairs.push([false, field, data, dataNode]); } else if((dataNode.ownerDocument && obj.isNodeParsed(dataNode, name))){ pairs.push([true, field, data, dataNode]); } else { success = obj.processAttribute(field, r, dataNode, params); //pairs.push([success !== undefined, field, r, data, dataNode, success]); if(success === undefined) pairs.push([false, field, r, data, dataNode]); else pairs.push([true, field]); } } }; for each(let root in altbase) this.nodes[name+"-count"] += $Z(f, func, root); } /*else if(alt.localized) { let out = languages.getValueObj(alt.localized, altbase); if(out){ this.setNodeParsed(out.text, name) this.nodes[name+"-node"] = out.node; this.nodes[name+"-value"] = out.text; alt.parse.call(this, out, alt.localized, name, alt); } }*/ else if(alt.parse) { //function(r, i, obj, name, alt, results){} or function(node, i, obj, name, inst, results){} this.nodes[name+"-count"] = 0; //[r, i, obj, name, alt, results] + params params = [compileXPath(alt.xpath), alt.parse, null, this, name, alt, results].concat(params); for each(let root in altbase) { params[2] = root; this.nodes[name+"-count"] += $Z.apply(null, params); } } else { temp = compileXPath(alt.xpath || alt); let first = !alt.all; outer: for each(let root in altbase) for each(let node in $W(temp, root)) if(this.processAttribute(name, null, node, params) !== undefined && first) break outer; } if(results.length) this.nodes[name+"-results"] = results; } } }, processAttribute:function(field, titleNode, valueNode, params) { let realValueNode = this.readAttributeValueNode(field, valueNode, params); if(realValueNode !== undefined) { if(titleNode && titleNode.ownerDocument){ this.nodes[field+"-title"] = titleNode; this.setNodeParsed(titleNode, field); titleNode.setAttribute("title-node", field); } this.nodes[field+"-node"] = valueNode; if(realValueNode.ownerDocument) { this.nodes[field+"-value"] = realValueNode; this.setNodeParsed(realValueNode, field); } else if(isArray(realValueNode)) { this.nodes[field+"-values"] = realValueNode; for each(let rvn in realValueNode){ this.setNodeParsed(rvn, field); } } if(valueNode.ownerDocument) { this.setNodeParsed(valueNode, field); } else if(isArray(valueNode)) { for(let vn of valueNode) { this.setNodeParsed(vn, field); } } } return realValueNode; }, readAttributeValueNode: function(field, valueNode, params) { function CleanupTextFormating(node){ node.classList.add("cleaned"); function bludgen(value, r, ft){ switch(value) { case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": case "*": case "\x95": r.nextSibling.classList.add("list"); if(ft && ft.nextSibling) ft.nextSibling.classList.add("list"); case "(": break; default: if(!r.nextSibling.classList.contains("list")) r.nextSibling.classList.add("hide-break"); } } function go(r){//I know in general what this code does but I don't recall the specifics. let v = r.nodeValue.trim(); let ft; for(ft = r.nextSibling; ft && !ft.nodeValue; ft = ft.nextSibling); if(!ft) return; let last, first; switch(last = v.slice(-1)) { case "-": r.nodeValue = r.nodeValue.slice(0,-1); remove(r.nextSibling); case ":": case ".": case ""://damn whitespace return; default: if(last.toLowerCase() == last) bludgen(ft.nodeValue.trim()[0], r, ft); } bludgen(first = v[0], r);//I don't know why we don't pass this ft } $Z(compileXPath(".//text()[following-sibling::node()[1][self::br and not(following-sibling::node()[1][self::br])]]"), go, node) } params = params || [];//can't be null! let classAdd = []; let success = true; let value = undefined, m, a, x; let obj = this; switch(field) { case "Download":{ if(a = $X(compileXPath("ancestor-or-self::a[1]"), valueNode)) { this.downloadURL = a.href; x = search2obj(a.search, true); if(!obj.id) obj.id = x.reserveid; if(!obj.format && (m = Number(x.formatid)) != -1) obj.formats.push(m); } break; } case "formatGroup":{ obj.formatGroup = String.prototype.slice.apply((valueNode.value || valueNode.textContent).trim(), params); break; } //case "Min. Version": { break; } //case "DRM Level": { break; } //case "":{ break; } case "Table of Contents":{ x = []; value = []; $Z(compileXPath(".//ul/li"), function(r,i){ value.push(r.textContent.trim()); x.push(r);}, valueNode); if(x.length){ valueNode = x; obj.toc = value; } break; } case "Sample":{ if(valueNode && valueNode.nodeType == valueNode.ATTRIBUTE_NODE){ this.sample = success = valueNode.value; } else { valueNode = $X(compileXPath("descendant-or-self::a"), valueNode) || valueNode;// | parent::link // if(valueNode.title) // this.sampleType = valueNode.title.trim(); if(valueNode.href){ this.sample = valueNode.href; if(!this.id && (m = this.sample.match(find_id))) this.id = m[1];//this is cheating I know. I'm too lazy to parse the URL } } break; } case "Short Description": case "Full Description":{ if(!Array.isArray(valueNode)) valueNode = [valueNode]; valueNode.forEach(CleanupTextFormating); } case "Description":{ if(!Array.isArray(valueNode)) valueNode = [valueNode]; let expr = /^(\d+[\.\)]*)\s+(?:(?=.*[^\)]$)(.*)|(.*) (\(((?:[^\(\)]*|\([^\(\)]*\))+)\)))$/; let tracks = []; valueNode.forEach(function(r){ // CleanupTextFormating(r); let text = $X(compileXPath(".//text()[contains(., 'Download Includes:')]", r)); if(text) { let values = []; function qp(r){ let line = insertAsParent(document.createElement("div"), r); line.className = "track"; if(line.nextSibling && line.nextSibling.nodeName == "BR") remove(line.nextSibling); let m = expr.exec(r.nodeValue.trim()); if(m) { let b = insertAsParent(document.createElement("b"), SplitTextNode(r, m[1])); b.className = "track-number"; b.title="Track number"; tracks.push(m[2] || m[3]); values[m[1]] = line; if(m[5]) { let wrap = SplitTextNode(b.nextSibling, m[4]); let span = insertAsParent(document.createElement("span"), wrap); let unwrap = SplitTextNode(wrap, m[5]); let i = insertAsParent(document.createElement("i"), unwrap); span.className = "creator"; span.title = "Track artist"; } let title = insertAsParent(document.createElement("span"), SplitTextNode(b.nextSibling, m[2] || m[3])); title.className = "track-title"; title.title = "Track title"; } } r.classList.add("renumbered"); let textbox = $X(compileXPath("ancestor-or-self::node()[count(parent::node()/node()) > 1][1]"), text); if($X(compileXPath("following-sibling::ol"), textbox)) $W(compileXPath("following-sibling::ol/li/text()"), textbox).forEach(function(m){tracks.push(m.nodeValue.trim());}); else if(!$Z(compileXPath("following-sibling::text()[string-length(translate(., ' ', '')) > 0]"), qp, textbox)) $Z(compileXPath("ancestor::p/following-sibling::p/text()"), qp, text); //(not(self::br) and .//text()[string-length(translate(., ' ', '')) > 0]) if(values.length > 1){ var after = values[1].previousSibling; values.forEach(function(v){if(v){insertAfter(v, after); after = v}}); } } }); if(tracks.length) obj.tracks = tracks; } case "If you like this title, you might also like...": { let objs = []; let f =compileXPath("self::table/tbody/tr/td"); let parse = function(r){ let obj = new Item([r]); obj.getAdditional({ Image:".//a[starts-with(@href, 'ContentDetails.htm')]/img", Title:{xpath:"ancestor-or-self::*[parent::td][1]/following-sibling::*[descendant-or-self::a[starts-with(@href, 'ContentDetails.htm')]]", node:"Image-node"}, Creator:{xpath:"following-sibling::div[not(a/img)]", node:"Title-node"}, Author:"text()[contains(., 'by ')]/following-sibling::*[self::a]", Sample:"div/a[starts-with(@href,'http://excerpts.contentreserve.com/') and img]", }); obj.enhance(); objs.push(obj); } for each(var table in valueNode) $Z(f, parse, table); obj.suggestions = objs; break; } case "Geographic Rights": { if(success = valueNode.textContent.trim()) { this.countries = success.replace(/'S/g,"'s").split(/\s*,\s*(?![^,]+?\s[Oo]f(?:\s[Tt]he)?,)/); while((value = this.countries.indexOf("Virgin Islands")) > -1)//not sure how to catch this case in the split this.countries.splice(value, 2, this.countries.slice(value, value + 2).join(", ")); m = valueNode; valueNode = []; for(let country of this.countries) if(value = SplitTextNode(findTextElementContaining(country, m), country)){ valueNode.push(value); m = value.nextSibling;//If a value is repeated it won't properly split otherwise. } } break; } case "SmartLink":{ if(success = (valueNode.search || false)){ let search = search2obj(valueNode.search, true); let temp = {}; //let searches = []; do{ objFilter(search, ["id", "format"], temp)//I don't like this method nor do I trust it 100%, but it will work without any problem when used on WishListRemove //searches.push(search); }while(search.url && (search = search2obj(search.url.split("?").slice(-1)[0], true)));//if(search.action != "WishListRemove")//nested in a login link! objMerge(this, temp); success = objHasFields(temp, params); //log(searches, this) } break; } case "Creator Marketing":{ value = value || "creatorMarketing"; } //case "Excerpts":{ value = value || "excerpts"; }//doesn't work for libraries (not that we really care about parsing this) case "Reviews":{ value = value || "reviews"; var half = []; if(valueNode.ownerDocument) {//overdrive half = doWhile(valueNode, "ancestor::tr[1]/following-sibling::tr[1]/td[table and count(*)=1]") .map(function(node){return {name:$X(compileXPath("table/tbody/tr[1]/td"), node), value:$X(compileXPath("table/tbody/tr[3]/td[2]"), node), base:node}}); } else {//library for(var i = 0; i < valueNode.length - 1; i++) { let name = $X(compileXPath("self::div[i]"), valueNode[i]), val = $X(compileXPath("self::div"), valueNode[i + 1]); //log(name, val); if(name && val) { half.push({name:name, value:val}) i++; } } } if(half && half.length){ this[value] = {}; valueNode = []; for each(var nodes in half) { if(nodes.name && nodes.value){//doesn't always have a value in the first node. if(nodes.base) valueNode.push(nodes.base); else{ valueNode.push(nodes.name); valueNode.push(nodes.value); } let c = nodes.name.textContent.trim(); let b = c.replace(/^(?:\x97|—|\u2014)?(.*?)(?:[\s]*[\.]{3})?$/, "$1"); let name = b.split("\n")[0].trim(); // log(c, b, name, nodes.name, c[0], c.charCodeAt(0)); nodes.name.setAttribute(value, "name"); nodes.value.setAttribute(value, "value"); let img = $X(compileXPath(".//img"), nodes.name); if(img) { nodes.image = img; if(img.alt) name = img.alt; } this[value][name] = m = {/**/nodes:nodes,/**/ name:name, html:nodes.value.innerHTML.trim()}; let a = findTextElementContaining(name, nodes.name, null, "parent::a"); if(a && a.href) m.href = a.href; let x = $X(compileXPath(".//a[.//text()[contains(., 'photo')]]"), nodes.name) if(x && x.href) m.photo = ReadJavascriptHref(x, "href").params[0]; } } } break; } case "ASIN":{ value = value || "ASIN"; } case "Lib. ISBN": case "Ret. ISBN": case "ISBN":{ value = value || "ISBN"; this[value] = String.prototype.slice.apply((valueNode.textContent || valueNode.value).trim(), params); break; } case "Released": case "Release date":{ this["release-date"] = String.prototype.slice.apply(valueNode.textContent.trim(), params); break; } case "Date added":{ this["date-added"] = String.prototype.slice.apply((valueNode.textContent || valueNode.value).trim(), params); break; } case "Edition":{ //log(arguments) switch(obj.edition = String.prototype.slice.apply((valueNode.textContent || valueNode.value).trim(), params)){ // case "Subtitled in English": case "Unabridged":{ if(valueNode) getTag(valueNode).classList.add("field-unabridged"); classAdd.push("item-unabridged"); break; } case "Abridged"://{ case "Slightly Abridged": case "Unabridged Selections":{ if(valueNode) getTag(valueNode).classList.add("field-abridged"); classAdd.push("item-abridged"); break; }//} } break; } case "Library Provider(s)": this.provider_type = -2;//library case "Retailer(s)"://content reserve case "Retail Provider(s)": this.provider_type += 1;//retail case "Available at":{//search this.provider_groups.push(field); m = valueNode; valueNode = []; $Z(compileXPath(".//a[@href != '']"), function(a){ if(!a.host)//try to fix it! a.href = a.href.split(" ")[0]; if(a.host && a.host != server){//just to make sure it's not a local link. let provider = Providers.getProvider(a.host); let span = insertAsParent(document.createElement("span"), a); let bool = null; switch(provider.mode) { case "Active": case "Include":{ a.classList.add("subscription-active"); obj.subscription = "active"; bool = true; break;} case "Inactive":{ a.classList.add("subscription-inactive"); if(obj.subscription != "active") obj.subscription = "inactive"; bool = false; break;} case "Exclude": default:{ a.classList.add("subscription-none"); break;} } let ov = /^(?:www\.)?(?:overdrive|dlrinc|libraryreserve)\.com$/.test(a.host) if(ov) a.classList.add("subscription-overdrive"); span.className = a.className; switch(provider.type){ case "integrated": if(a.host != provider.host) a.host = provider.host; //some have added junk that is bogus... i'm hoping this works for all. a.pathname = "/"+a.pathname.split("/").slice(-1); break; case "isolated": if(x = provider.createLink("title", obj)) a.href = x.href; else log("not enough data to construct title link", obj); break; } /* var po = {name: a.textContent.trim(), href: a.href, a:a}; /*/ let text = a.textContent.trim(); let po = {name: provider.data.title || text, href: a.href, a:a, overdrive:ov}; a.innerHTML = po.name; if(ov){ let od = /(OverDrive Catalog|Digital Library Reserve, Inc.)(?: - (\S*)(?: - (.*))?)?/.exec(text); od && od[1] && (po.name = od[1]) && od[2] && (po.country = od[2]) && od[3] && (po.company = od[3]); } //*/ if(bool !== null) po.supported = bool; if(po.country){ var arr = obj.provider_countries[po.country]; if(arr == null) obj.provider_countries[po.country] = arr = []; arr.push(po); span.setAttribute("country", po.country); a.setAttribute("country", po.country); } /*if(po.company){ var arr = obj.provider_company[po.company]; if(arr == null) obj.provider_company[po.company] = arr = []; arr.push(po); span.setAttribute("company", po.company); a.setAttribute("company", po.company); }*/ obj.providers.push(po); if(x = $X(compileXPath("./following-sibling::text()[1]"), span)) span.appendChild(x); //TODO //var textNode = valueNode.push(span); } }, m); if(!obj.hasOwnProperty("subscription")) obj.subscription = "unavailable"; break; } case "Available for library lending":{ this.provider_type = -1; break; } case "Formats":{ x = []; value = []; $Z(compileXPath("./li/@value | ./li/@data-allformats"), function(r,i){ value.push(Number(r.value)); x.push(r.ownerElement);}, valueNode); if(x.length){ success = valueNode = x; if(Array.isArray(obj.formats)){ for(let a of value) if(obj.formats.indexOf(a) == -1) obj.formats.push(a); } else { log("you collected them but didn't do anything with them", value); } } break; } case "FormatIcon":{ if((m = valueNode.getAttribute("src").match(/\/Content\/images\/formatIcons\/Format([\d]*)Thumb\.gif/))) { value = Number(m[1]); if(isNaN(value)) value = undefined; if(value === undefined) break; } } case "FormatHelp":{ if(!value && (m = valueNode.getAttribute("href").match(/Help-Reader-Format([\d]*).htm/))) { value = Number(m[1]); if(isNaN(value)) value = undefined; } if(value === undefined) break; } case "FormatType": case "Format Infomation": case "Format":{ if(!value && ((valueNode.nodeName == "INPUT" && valueNode.type == "hidden") || (valueNode.nodeType == valueNode.ATTRIBUTE_NODE)) && valueNode.value) value = Number(valueNode.value); var format = value || Formats.Names[ a = String.prototype.slice.apply(valueNode.textContent.trim(), params) ]; if(success = (format != null)) { var type = Formats.Numbers[format].type; if(obj.formats && obj.formats.push){ if(obj.formats.indexOf(format) == -1) obj.formats.push(format); } else { throw new Error("i thought this code was dead!"); } } else if(a) {//HACK!!! We aren't told what specific formats, but we need to know so we can filter. for each(var c in Formats.Classes){ if(c.title == a){ if(this.formats && this.formats.push){/*/ for each(let f in c.formats) if(this.formats.indexOf(f) == -1) this.formats.push(f);/*/ this.formats.push(a); /**/ } else { throw new Error("i thought this code was dead!"); } success = true; break; } } } break; } case "Supplemental Formats":{ var supplements = []; //this is specific to a particular webpage. $Z(compileXPath("table/tbody/tr[td[@colspan]]"), function(w){ let box = new Item(doWhile(w, "./following-sibling::tr[position() = 1 and not(td[@colspan])]")); box.getAdditional({Pairs:{field:"td[1]/descendant-or-self::*[contains(text(), ':')]", value:"ancestor::tr[1]/td[2]"}}); box.enhance(); supplements.push(box); }, valueNode); this.supplements = supplements; break; } case "Author": case "Creator":{ m = ((valueNode.textContent != null)?valueNode.textContent:valueNode.value).trim(); if(!params.length && m.match(/^by\s/)) params = [3]; if(success = String.prototype.slice.apply(m, params).trim()){ value = SplitTextNode(findTextElementContaining(success, valueNode), success) || undefined; if(m = $X(compileXPath("(ancestor::a)[starts-with(@href, 'BANGSearch.dll?')]"), value)){ let num = search2obj(m.getAttribute("href").slice(15), true).id; this.creatorObjs = [find_id.exec(num)?{name:success, id:num, link:m, text:value}:{name:success, number:num, link:m, text:value}]; }else this.creatorObjs = [{name:success, id:undefined, number:undefined, link:undefined, text:value}]; } //else if(success != null)//I found a title without an author but it still has the place holder for it. Very strange // this.creatorObjs = [{name:success, id:undefined, text:undefined}]; break; } case "Creator(s)": case "Creators":{ if(valueNode.nodeName == "TABLE") { if(success = (valueNode = $W(compileXPath("tbody/tr/td[1]"), valueNode)).length) { this.creatorObjs = []; for(let node of valueNode){ let creator = node.textContent.trim(); value = SplitTextNode(findTextElementContaining(creator, node), creator) if(value && (a =$X(compileXPath("ancestor::a[@href]"),value))){ let num = search2obj(a.getAttribute("href").slice(15), true).id; this.creatorObjs.push(find_id.exec(num)?{name:creator, id:num, link:a, text:value}:{name:creator, number:num, link:a, text:value}); } else this.creatorObjs.push({name:creator, id:undefined, number:undefined, link:undefined, text:value}); } } } else if(valueNode.nodeName == "UL") { if(success = (valueNode = $W(compileXPath("li/span"), valueNode)).length) { this.creatorObjs = []; for each(let node in valueNode){ let [role, creators] = node.textContent.trim().split(/:\xA0\s*/g); for(let creator of creators.split(/\s*,\s*/)) { value = SplitTextNode(findTextElementContaining(creator, node), creator) this.creatorObjs.push({name:creator, role:role, id:undefined, number:undefined, link:undefined, text:value}); } } } } else if($W(compileXPath("descendant-or-self::*[br and a]/a"), valueNode, success = []).length > 0){ valueNode = success; this.creatorObjs = []; for each(let a in success){ let creator = a.textContent.trim(); if(creator) { let num = search2obj(a.getAttribute("href").slice(15), true).id; this.creatorObjs.push(find_id.exec(num)?{name:creator, id:num, link:a, text:a}:{name:creator, number:num, link:a, text:a}); } else this.creatorObjs.push({name:creator, id:undefined, link:undefined, text:a}); } } else if(success = valueNode.textContent.trim()) { let names = success.split(",") .map(function(s){return s.trim();}) .filter(function(v){return v != "na Not Applicable";}); m = valueNode; valueNode = []; this.creatorObjs = []; for each(let creator in names){ valueNode.push(value = SplitTextNode(findTextElementContaining(creator, m), creator)); if(value && (a =$X( compileXPath("ancestor::a[@href]"),value))) { let num = search2obj(a.search, true).id; this.creatorObjs.push(find_id.exec(num)?{name:creator, id:num, link:a, text:value}:{name:creator, number:num, link:a, text:value}); } else this.creatorObjs.push({name:creator, id:undefined, link:undefined, text:value}); } } break; } case "Language(s)":{ if(valueNode.nodeName == "TABLE") { if(success = (valueNode = $W(compileXPath("tbody/tr/td[1]"),valueNode)).length) this.languages = valueNode.map(function(c){return c.textContent.trim();}); } else if(success = valueNode.textContent.trim()) { this.languages = success.split(",").map(function(s){return s.trim();}); m = valueNode; valueNode = []; for each(let language in this.languages) if(value = SplitTextNode(findTextElementContaining(language, m), language)) valueNode.push(value); } break; } case "Size":{ value = valueNode.textContent.match(/\(((?:\d+|\(unknown\)) [yzeptgmk]?b)\)/i)[1]; } case "File Size": case "File size":{ var text = (value || (value = valueNode.textContent)).trim().split(" "); //let mult = params || 1024; let size; if(text[0] == "(unknown)") { text[1] = "b"; size = "unknown"; } else size = Number(text[0]); switch(text[1].toLowerCase()) { case "yb"://does the script really need to be this future proof? size = size * 1024; case "zb": size = size * 1024; case "eb": size = size * 1024; case "pb": size = size * 1024; case "tb"://this isn't likely size = size * 1024; case "gb"://this could happen size = size * 1024; case "mb": size = size * 1024; case "kb"://in reality, i've never seen anything other than kb, even when another denomination would make more sense. this["approximate-size"] = Math.round(size * 1024);//They started using rounded decimal places. break; case "b": this["approximate-size"] = this.size = size; break; default: success = false; } break; } case "Series":{ valueNode = $X(compileXPath("a"), valueNode) || valueNode; var text = valueNode.textContent.trim(); if(text != "Abridged")//Catch a data entry mistake. { if(valueNode.href && (m = valueNode.href.match(find_id))) this["series-uuid"] = m[1]; if(text.match(/ Series$/))//Series in the series field is superflous (and confuses things) text = text.slice(0,-7); // log(text) this['series-sort'] = this.textSort(this.series = text); } //log(text); break; } case "Subtitle":{ this.subtitle = valueNode.textContent.trim(); break; } case "Average rating": case "My rating":{ valueNode = $X(compileXPath("following-sibling::ul[1]"), rr) break; } case "Library copies":{ //Number.POSITIVE_INFINITY; if(success = $X(compileXPath("noscript/text() | text()[count(../node()) = 1]"), valueNode)) this.libraryCopies = (((value = (valueNode = success).textContent.trim()) == "always available")?Number.POSITIVE_INFINITY:Number(value)); break; } case "Title":{ valueNode = $X(compileXPath("a"), valueNode) || valueNode; if(success = (valueNode.textContent || valueNode.value)) { this.title = String.prototype.slice.apply(success.trim(), params); } if((m = ReadJavascriptHref(valueNode, "href")).success && m.name == "TitleInfo") { this.id = m.params[0]; this.formats.push(m.params[1]); } else if(valueNode.search && (m = valueNode.search.match(find_id))) this.id = m[1]; else if(valueNode.pathname && ((m = valueNode.pathname.split("/"))[1] == "ti") && (m = m[2].match(/^([\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12})-(\d+)-(\d+)-(\d+)-(\d+)-(\d+)$/)) ){//"c279485c-b814-461a-b8d5-944762dfbb95-425-1-1-1-1" this.id = m[1]; let format = Number(m[2]); //3 -> 6 pertain to the search //showEbooks=m[3] //showAudiobooks=m[4] //showMusic=m[5] //showVideo=m[6] if(this.formats && this.formats.push) if(this.formats.indexOf(format) == -1) this.formats.push(format); } break; } case "Img": case "Image":{ if(valueNode.nodeName === "NOSCRIPT") valueNode = evalNoScript(valueNode, document.createElement("div")); valueNode = $X(compileXPath("descendant-or-self::img"), valueNode) || valueNode;// | parent::link if(valueNode.alt && !this.title) { if(m = /^Click here to view (.*?) details for (.*) by (.*)$/.exec(x = valueNode.alt.trim())) { if(!this.creator && m[3]) this.creatorObjs = [{name:m[3], id:undefined, text:undefined}]; this.title = m[2]; } else this.title = x; } if(valueNode.getAttribute("src") == "system/spacer.gif" && valueNode.hasAttribute("data-original")){ //some script in charge of propegating this doesn't get called with autopaging. This is the wrong place for this but it requires the least amount of code to fix it here. //Otherwise I'd have to add preprocessing ugg. valueNode.src = valueNode.getAttribute("data-original") if(valueNode.style.height == "0px")//this is insane, why did they do this? valueNode.style.removeProperty("height"); } if(valueNode.hasAttribute("data-publisher")) this.publisherNumber = valueNode.getAttribute("data-publisher"); if(valueNode.src) this.image = valueNode.src; if(!this.id && (m = ((a = $X(compileXPath("ancestor::a[1]"), valueNode)) && a.search.match(find_id) || (valueNode.src && valueNode.src.match(find_id))))) this.id = m[1];//prefer the id in the link over the one in the image. but beggers can't be choosers. break; } case "ID":{ if(((valueNode.nodeName == "INPUT" && valueNode.type == "hidden") || (valueNode.nodeType == valueNode.ATTRIBUTE_NODE)) && valueNode.value) this.id = success = valueNode.value; break; } case "IdExtractor":{ if(!this.id && (m = ((a = $X(compileXPath("ancestor-or-self::a[1]"), valueNode)) && (a.search + a.hash).match(find_id) /*|| (valueNode.src && valueNode.src.match(find_id))*/))) this.id = success = m[1];//prefer the id in the link over the one in the image. but beggers can't be choosers. break; } case "Publisher":{ this.publisher = valueNode.textContent.trim(); break; } case "ServicerTypes":{ x = []; a = {}; if(m = $X(".//div/a[img[@alt='WorldCat']]", valueNode)) { a.worldcat = (m.parentNode.style.display != "none") && m.href.length && m.href; classAdd.push("worldcat-"+(a.worldcat?"available":"unavailable")); } else classAdd.push("worldcat-unknown"); if(m = $X(".//div[a/img[@alt='Find at a Library']]", valueNode)) { x.push(m); a.library = (m.style.display != "none"); classAdd.push("library-"+(a.library?"available":"unavailable")); } else classAdd.push("library-unknown"); if(m = $X(".//div[a/img[@alt='Find at a Bookstore']]", valueNode)) { x.push(m); a.store = (m.style.display != "none"); classAdd.push("store-"+(a.store?"available":"unavailable")); } else classAdd.push("store-unknown"); if(x.length){ this.servicerTypes = a; valueNode = x; } break; } case "WishList":{ if(!obj.title) obj.title = valueNode.getAttribute("title").slice(1,-1);//wrapped in single quotes if(!obj.id) x = valueNode.getAttribute("value"); if(m = $X(compileXPath("img"), valueNode)){ obj.wishlist = m.getAttribute("data-wishlist"); if(!obj.id) x = m.className; } if(!obj.id && x && (x = x.match(find_id))) obj.id = x[1]; break; } case "Recommend":{ x = {"image":"data-rtlimgsrc", "title":"data-rtltitle", "subtitle":"data-rtlsubtitle", "id":"data-rtlcriddata"}; for(let [i, v] in new Iterator(x)) { if(!obj[i] && valueNode.hasAttribute(v)) { obj[i] = valueNode.getAttribute(v); } } if(!obj.status) { if(m = $X(compileXPath("a"), valueNode)){ obj.status = "recommend" } else if(m = $X(compileXPath("img"), valueNode)){ obj.status = "recommended" } } break; } } // log(arguments); if(classAdd = classAdd.join(" ")) for each(var root in this.pieces) root.className = root.className ? root.className + " " + classAdd : classAdd; return (success || success === "")?valueNode:undefined; }, /*saveCopies: function(server_obj){ let obj = this; let dirty = false; if(obj.id && server_obj && server_obj.waitinglist) { let copies = []; let found; if(server_obj.copies && server_obj.copies.length) copies = server_obj.copies; if(Array.isArray(obj.formats)) { for each(var item in server_obj.waitinglist){ if(item.id == obj.id && obj.formats.indexOf(item.format) != -1) { copies = copies.filter(function(obj){return item.id != obj.id || obj.format != item.format;}).concat({ id:item.id, format:item.format, copies:obj.libraryCopies, loaded:new Date(document.lastModified).getTime(), }); dirty = true; } } } else { for each(var item in server_obj.waitinglist){ if(item.id == obj.id && (found = obj.formats[item.format])) { copies = copies.filter(function(obj){return item.id != obj.id || obj.format != item.format;}).concat({ id:item.id, format:item.format, copies:(this.copies !== undefined)?this.libraryCopies:found.libraryCopies, loaded:new Date(document.lastModified).getTime(), }); dirty = true; } } } if(dirty) server_obj.copies = copies; } return dirty; },/**/ installMetadataForFiltering: function(){ let node = this.pieces[0]; let div; if(node) { let old = $X(compileXPath("div[@class = 'HiddenSearchable']"), node) div = document.createElement("div") div.className = "HiddenSearchable"; //serialize for each([name, property] in new Iterator(this.translator)) { let value = GetFieldValue(this, property); if(value !== undefined){ if(Array.isArray(value)) { let div2 = div.appendChild(document.createElement("div")); div2.setAttribute("block", name); for each(let val in value){ let span = div2.appendChild(document.createElement("span")); span.setAttribute("parsed", name); span.setAttribute("typeof", typeof(val)); span.appendChild(document.createTextNode(name+":"+val)); div2.appendChild(document.createElement("br")); } } else { let span = div.appendChild(document.createElement("span")); span.setAttribute("parsed", name); span.setAttribute("typeof", typeof(value)); span.appendChild(document.createTextNode(name+":"+value)); div.appendChild(document.createElement("br")); } } } if(old) replace(div, old); else node.appendChild(div); } return div; }, },{ pieces: WrapNewArray, formats: WrapNewArray, nodes: WrapNewObject, creatorObjs: WrapNewArray, }, true), {//darn expensive to generate, so lets delay doing it until we have to. translator:function(){return { "title":"title", "series":function series(){ return this.Series || ""; }, "creator":"creators", "book":"series-position", // "added":function(){if(this.addedToWishlist){ }}, };}, subtitleParser:function(){//this function generates the value //way too complicated regexp //Christian History and Biography Issue 78, vol. 22, No. 2 //http://erl.lib.overdrive.com/BANGSearch.dll?Type=Creator&ID=364561 var series = "(?:\\s+(?:Series))?"; var type = "(?:[bB]ook|Vol(?:ume|)|Part|Episode|Novel|Issue|Chapter|No\\.)"; var number = "(#?\\s*[0-9]+(?:\.[0-9]+)?|One|Two|Three|Four|Five|Six|Seven|Eight|Nine|[IVXLCDMV]+)"; var numberNoCapture = "(?:"+number.slice(1); var thru = "\\s*?(?:-|\\s+(?:[Tt]o|[Tt]hrough|[Tt]hru)\\s+)\\s*"; let many = type+"s\\s+(?:"+[//{//multiple "("+numberNoCapture+"+(?:,\\s+"+numberNoCapture+")*),?\\s+(?:and|&)\\s+"+number, "([A-Y])"+thru+"([B-Z])", number+thru+number, ].join("|")+")";//} let manypos = ["comma", "single", "ras", "rae", "rns", "rne"];//6 var position = "("+[//{ type+"\\s*"+number + "(?:\\s+\\("+many+"\\)|)", //single many, "[cC]ollection\\s+"+number+thru+number,//grrrr no type! ].join("|")+")";//} var expr = "^(?:"+[//{ "A (.*?) Digital Collection: "+position,//1, 2-17 "(?:From\\s+|)(.*?),?"+series+"(?:[:,]\\s*|\\s+-\\s+|\\s+)"+position+"(?: of "+number+")?",//18, 19-34, 35 position + " of the (.*?)"+series,//36-51, 52 "(?:Stories from|A Novel of) (.*)",//53 "(An? (.*?) (?:Novel|Mystery|Story|Novella(?: (?!to ).*)?))(?::\\s*\\54|)",//54, 55 "(" + many + "): ((?:[^/]+?/)*[^/]+)",//56, 57, 58, 59, 60, 61, 62, 63 "(.*?) "+number+":\\s+(.*)",//64, 65, 66 "(.*?)"+series+"\\s+(#[1-9][0-9]*)",//67, 68 "(.*?) Series, (#?[1-9][0-9]*)",//69, 70 "(Part ([1-9][0-9]*))(?: of | ?/ ?)([1-9][0-9]*)",//71, 72, 73 "(" + many + ")",//74, 75, 76, 77, 78, 79, 80 "(.*?)(?: Audio)? (Collection)",//81, 82 "(.*?) \\((.*?), "+position+"\\)",//83, 84, 85-100 don't forget to properly escape slashes!!! "(Volume ([1-9][0-9]*[A-Z]))",//101, 102 ].join("|")+")$";//} var pos = ["positionText", "single", ...manypos, ...manypos, "rns", "rne",];//16 var map = ["all", //{ //0 "series", ...pos,//1, 2-17 "series", ...pos, "seriesTotal",//18, 19-34, 35 ...pos, "series",//36-51, 52 "stories", //53 null, "stories", //54, 55 "positionText", ...manypos, "titles",//56, 57-62, 63 "series", ["positionText", "single"],"title", //64, 65, 66 "series", ["positionText", "single"], //67, 68 "series", ["positionText", "single"], //69, 70 "positionText", "single", "seriesTotal",//71, 72, 73 "positionText", ...manypos, //74, 75, 76, 77, 78, 79, 80 "stories", "collection",//81, 82 "title", "series", ...pos, //83, 84, 85-100 "positionText", "single",//101, 102 ];//} var parse = {single:parseNumber, comma:parseComma, rns:parseNumber, rne:parseNumber, seriesTotal:parseNumber, titles:parseTitles}; function parseRomanFast(str){//WARNING: Does not check for input validity var numerals = {I:1, V:5, X:10, L:50, C:100, D:500, M:1000}; for(var last = 0, total = 0, i = str.length - 1; 0 <= i; i-- ) { var value = numerals[str[i]]; total = (value < last)?(total - value):(total + (last = value)); } return total; } function parseNumber(raw){ var e = /^\s*(?:#?\s*([0-9]+(?:\.[0-9]+)?)|([IVXLCDMV]+)|(One|Two|Three|Four|Five|Six|Seven|Eight|Nine))\s*$/.exec(raw); if(e === null) return raw; var [value, digit, roman, word] = e; if(roman) return parseRomanFast(value); if(word) return {One:1,Two:2,Three:3,Four:4,Five:5,Six:6,Seven:7,Eight:8,Nine:9,}[value]; return Number(digit); } function parseComma(value){ return value.split(",").map(parseNumber); } function parseTitles(value){ return value.split(" / "); } var r = execWithNameAccessors(new RegExp(expr), map, parse) //"A Companion Novella to Cry Wolf" do no parse this! //A Bartimaeus Novel: A Bartimaeus Novel //A Wild About You Novella An eSpecial from New American Library //Aurora and the Helpful Dragon (Princess Hearts Classic Storybook #3) http://search.overdrive.com/classic/retail/TitleInfo.asp?TitleReserveID=%7B85DB024A-6B64-4EC2-97C2-7EFC9DF01355%7D&TitleFormatID=302 //let t = "Volumes One Through Three: The Sword of Shannara / The Elfstones of Shannara / The Wishsong of Shannara"; //log({a:[t, r.exec(t), "Volumes One Through Three".match(new RegExp(many))]}); return r; }, }, undefined, false); //} //{ FormatItem /*/ function FormatItem(parent, pieces) { Item.call(this, pieces); this.pieces;//do not remove these three lines, they ensures child and parent are not confused. this.nodes; this.formats; // this.format = null; this.__proto__ = parent; } /*/ function FormatItem(parent, pieces) { let t = function(pieces){ //do not remove these lines, they ensures child and parent are not confused, despite child inheriting everything else from the parent. let p = Object.getPrototypeOf(parent); for each(var name in ["pieces", "nodes", "formats"]) Object.defineProperty(this, name, getPropertyDescriptor(p, name)); Item.call(this, pieces); // redirectGetter(p, "format", this); }; t.prototype = parent; return new t(pieces); } /**/ FormatItem.prototype = objCreateWithSelfDestructingAccessors(Item.prototype, null, {}); //} //{ ServiceItem function ServiceItem(book, splitter, id, additional){ Item.call(this); if(book) this.addPieces(book); this.pieces = this.pieces.reverse();//solves sorting problem later! if(splitter && this.addPieces(splitter)) splitter.classList.add("item-splitter"); this.pieces = this.pieces.reverse();//solves sorting problem later! if(id && (!additional || !additional.ID)) this.getAdditional({ID:id}); if(!additional || !additional.Image) this.getAdditional({Image:".//img[(@name='coverImg' or @name='cover' or (not(@name) and string-length(@alt) > 0)) and @src]"});// | ancestor::html/head/link[@image_src]/@href if(additional) this.getAdditional(additional); let t; if(!this.id && (t = location.search.match(find_id))) this.id = t[1]; this.enhance(); } ServiceItem.prototype = objCreateWithSelfDestructingAccessors(Item.prototype, { subscription:"unavailable", titleLinkType:"title-search", provider_type:0, }, { providers: WrapNewArray, provider_groups: WrapNewArray, provider_countries: WrapNewObject, }); //} //{UpdateURL function UpdateURL(name, params){ this.name = name; this.search = params ? "?" + obj2post(params) : ""; } Provider.prototype = { getURL: function(host) { return "http://" + this.host + "/" + this.name +".htm" + this.search; } }; //} //{ Provider function Provider(host, mode, local){ this.host = host; this.mode = mode; this.local = host && local; ProviderBase.call(this); // this.parsers = Array.slice(arguments, 2).concat(); switch(host){ // default: return; case "www.booksonboard.com": quickSetValueProperty(this, "type", "isolated"); quickSetValueProperty(this, "linkTypes", {title:{href:"sellers/halfsteps/ir.php?CRID={id}-{format}&sellerID=0996-0001", fields:{format:"format", "id":function(){return this.id.match(find_id)[1];},}, abbr:"T"}}); break; } } Provider.prototype = objCreate(ProviderBase.prototype, { type: "integrated", linkify: function(obj){ ProviderBase.prototype.linkify.call(this, obj); if(this.mode == "Include") this.getDataCache().ignorable(obj); }, getDataCache:function(){ return this.Data || (this.Data = this.data); }, saveData:function(){ return this.Data?(this.data = this.Data):(this.Data = this.data); }, get data(){//using this needs to be phased out in favor of getDataCache and saveData. var text = GM_getValue(this.host, ""); try { var obj = text && JSON.parse(text) || {}; } catch(e){ var obj = {}; } obj.__proto__ = Provider.ProviderData || installProviderData(); return obj; }, set data(v){//using this needs to be phased out in favor of getDataCache and saveData. if(!v || this.mode !== "Include") GM_deleteValue(this.host); else GM_setValue(this.host, JSON.stringify(v)); }, updateHost:function(host){ if(host != this.host){ let old = this.host; let data = this.Data || this.data; this.host = host; GM_deleteValue(this.host); this.data = data; } }, housekeeping:function(page, signed_in){ let dobj = this; let server_obj = this.getDataCache(); let now = new Date().getTime(); if((dobj.mode == "Include") && server_obj) { dobj.data = server_obj; //infinate nesting is bad, so we catch it via the hash if(!dobj.noAutoUpdate) { if(server_obj.secure == "libraryreserve" || (server_obj.features && server_obj.features.waitinglist)) this.update_page(page, "max", DefaultCheckoutPeriod); this.update_page(page, "formats", DefaultCheckoutPeriod); // this.update_page(page, "features", DefaultCheckoutPeriod); if(signed_in) { this.update_page(page, "waitinglist", 1); if(this.update_page(page, "checkedout", 1) == -2 && (server_obj.checkedout.sort(function(a, b){ return a - b; })[0] - now) < 0) this.update_page(page, "checkedout"); } } } }, update_page: function(page, name, days){ let load = this.update_page_map[name]; if(!load) return -3; let url = load.getURL(this.host) + "#embedded"; if($X(compileXPath("//iframe[@src='"+url+"']"))) return -1; let server_obj = this.data; let now = new Date().getTime(); if((page == load.toLowerCase()) || (name && server_obj[name] && !((days * 86400000) < (now - server_obj[name+"_load"])))) return -2; if(!document.body) return 0; // log(load) var iframe = document.createElement("iframe"); iframe.frameBorder=iframe.width=iframe.height="0"; // iframe.docShell.allowImages = false;//disable image loading - don't need them // iframe.docShell.allowAuth = false;//extra security iframe.src = url; document.body.appendChild(iframe); return 1; }, linkTypes:{ enhanced:{href:"EnhancedDetails.htm?id={id}", fields:["id"], abbr:"E"}, publisher:{href:"BANGSearch.dll?Type={type}&ID={publisherNumber}&PerPage={page}", fields:["publisherNumber"], pairs:{type:"Publisher", page:QuickSearchsCount}, title:"Search for other titles by this publisher", abbr:"P"}, series:{href:"BANGSearch.dll?Type={type}&ID={id}&PerPage={page}", fields:["id"], pairs:{type:"Series", page:QuickSearchsCount}, title:"Search for other titles in this series", abbr:"S"}, creator:objMerge([ {href:"BANGSearch.dll?Type={type}&ID={id}&PerPage={page}", fields:{"id":["creatorNumber", "creatorId"]}, pairs:{type:"Creator", page:QuickSearchsCount}}, {post:"BANGSearch.dll?URL=SearchResultsGrid.htm", fields:{"Creator":"creator"}, optional:{}, serialize: emQuote, extras:{"PerPage":QuickSearchsCount, "Sort":"SortBy=Relevancy"},}, ],{title:"Search for other titles by this creator", abbr:"C"}), title:[ {href:"ContentDetails.htm?id={id}", fields:["id"], abbr:"T"}, {post:"BANGSearch.dll?URL=SearchResultsGrid.htm", fields:{"Title":"title","Format":"Formats"}, escape:processForSort, optional:{}, abbr:"TS", serialize: emQuote, extras:{"PerPage":QuickSearchsCount, "Sort":"SortBy=Relevancy"}} ], "title-search":{post:"BANGSearch.dll?URL=SearchResultsGrid.htm", fields:{"Title":"title"}, optional:{}, abbr:"TS", serialize: emQuote, extras:{"PerPage":QuickSearchsCount, "Sort":"SortBy=Relevancy"}}, CartAdd:{href:"BANGCart.dll?Action={action}&ID={id}", fields:["id"], pairs:{action:"Add"}, title:"Add to Cart", abbr:"+C"}, CartRemove:{href:"BANGCart.dll?Action={action}&ID={id}", fields:["id"], pairs:{action:"Remove"}, title:"Remove from Cart", abbr:"-C"}, WishListAdd:{href:"BANGAuthenticate.dll?Action=AuthCheck&ForceLoginFlag=0&URL=BANGCart.dll%3FAction%3D{action}%26ID%3D{id}", fields:["id"], pairs:{action:"WishListAdd"}, title:"Add to Wishlist", abbr:"+W"}, WishListRemove:{href:"BANGAuthenticate.dll?Action=AuthCheck&ForceLoginFlag=0&URL=BANGCart.dll%3FAction%3D{action}%26ID%3D{id}", fields:["id"], pairs:{action:"WishListRemove"}, title:"Remove from Wishlist", abbr:"-W"}, }, parse:function(page, location, signed_in){ var sp = this; var server_obj = this.getDataCache(); var dirty = false; if(!page) (page = /\/([^.\/]+)\.htm$/.exec(location.pathname)) && (page = page[1].toLowerCase()); //if(!lang)//hack, too lazy to build the regex at the moment to detect the lang from the url. // lang = languages["default"]; //TODO put lang in dobj or dobj.data, the url is not always accurate. var title = $X("//td[@class='pghead'] | //div[@class='pageMainAreaHeader']"); var main = title && $X("ancestor::table[1]/..", title) || $X("//a[@name='maincontent']/following-sibling::table[last()]"); if(!title && main) title = $X("preceding-sibling::table[1]//*[string-length(text())>3]", main); //else if(!title) // title = $X("") if(title && title_link) title.innerHTML = "<a href='"+title_link+".htm'>"+title.innerHTML+"</a>"; let error; let errorType; if(error = (page == "error")){ switch((errorType = search2obj(location.search).ErrorType)) { case "1330": page = "mywaitinglist"; break; //maxed out on holds case "1320": page = "mywaitinglist"; break; //nothing being waited upon case "1000": page = "mybookshelf"; break; //nothing checked out case "750": break; //The title you are trying to view is protected and not available while signed out. Sign in to view your Bookshelf. case "210": break; //book not found, caused by a variety of reasons. case "200": //page = "cart"; break; case "220": page = "cart"; break; case "100": page = "searchresults"; break; case "320": page = "login"; break;//We're sorry, but the specified library patron account information is not valid. Details: Please try your card again or contact support. (Details: Failed HTTP Request to Library's ILS server.) } } if(errorType) errorType = Number(errorType); log("page: " + page) var title_link = page; if(!server_obj) error = true; if(title && title.innerHTML == "Invalid Page Request") error = true; var page_mode = 0; var page_array = []; var done = true; let ContentBase = $X("//*[comment()[.=' ************************* BEGIN PAGE CONTENT ************************* ' or .=' ************************** END PAGE CONTENT ************************** '] or table[@class='breadCrumbs'] or a[@name='maincontent'] or img[@id='WishListCmd']]"); GM_addStyle([//{ "table[parsed='If you like this title, you might also like...'] tbody { vertical-align:top; }", "span.links, .links .sup, span.links > div.links { font-size:9px!important; vertical-align: top; }", "span.right { float:right; margin: 0;}", "span.links { margin-left: 1px;}", ".pageturn-img, #coverOverlay { opacity: 0.5; }", ].join("\n"));//} switch(page) { //PerPage works on MyAccount.htm case "recommend":{ if(!error) { var obj = new Item(); obj.getAdditional({ Recommend:{form:"//form[@action='BANGAuthenticate.dll?Action=EmailRecommendation']", map:{"reserve_id":"ID", "title":"Title", "primary_creator_name":"Author"}}, Title:{xpath:"preceding-sibling::p/strong[u]", node:"Recommend-form"}, Creator:{xpath:"following-sibling::text()", node:"Title-node", params:[5, -65]}, // Pairs:{field:"../tbody/tr/td[1]", value:"following-sibling::td[last()]/*[self::b or self::strong]", node:"LibraryWaitingList-form"}, // DeComment:{xpath:"../tbody/comment()", parse:function(r){ (evalNoScript(r, null, ["table", "tbody"], true), arguments)}, node:"LibraryWaitingList-form"}, // Subtitle:{xpath:".[contains(.//text(), ': ')]", node:"Title-value"},//no subtitle if there is no splitter. }); obj.enhance(); sp.linkify(obj); if(obj.nodes["Title-node"])//not enough info for a full title link. obj.nodes["Title-node"].appendChild(OverDrive.createServiceLinks(document.createElement("span"), obj, "title")).className = "links";//"title-search" log(obj); } break; } case "contentdetails-streetdate":{//URL available var obj = new Item(["//div[@id='iframeHolder']"]) obj.id = search2obj(location.search, true).id; obj.formats = {}; obj.getAdditional({ Title:{xpath:"//strong[starts-with(text(),'What are the release date(s) for')]", params:[34, -2]}, Formats:{xpath:"table/tbody/tr", parse:function(r){ var f = new FormatItem(obj, [r]); f.id = obj.id; f.getAdditional({ Format:{xpath:"td[1]", params:[0, -1]}, "Release date":"td[2]/noscript", }); obj.formats[/*f.format = */f.format] = f; // delete f.formats; return r; }}, }); obj.enhance(); sp.linkify(obj); log(obj); break; } case "contentdetails-coll": //saw this one in the wild, had a section expanded. Is this an edge case? case "contentdetails":{ //TODO: handle library advantage //http://library2go.lib.overdrive.com/ContentDetails.htm?ID=E8FF68E3-F9F8-4C3D-A9DC-58F0A1BA72FA GM_addStyle([ "div.mainContainer div.row > div.columns.six { width:65%; }", "div.mainContainer div.row > div.columns.six + div.columns.six { width:35%; }", ].join("\n")); title_link = null; let base = ContentBase; let obj = new Item([base]); obj.id = search2obj(location.search, true).id; //obj.formats = {}; //obj.titleLinkType = "title-search" let version = $X("id('cover_img')") != null; /* //how annoying ~_~ should probably add h2 and h4 let titles = ["h1", "h3", "h5"]; let selfs = "[not("+titles.map(function(n){return "self::"+n;}).join(" or ") + " or self::b)]"; let expr = [//{ //easier to read spread out this way. "./following-sibling::*[1]"+selfs, "./following-sibling::*[1][self::b]/*[1]"+selfs,//the html quality is horrible!!! read into b tags as if they aren't there. // and not(parent::b) //"./parent::b/following-sibling::*[1][not(self::h1 or self::h5 or self::b)]",//having dug into the b, this digs out. this is not needed because of how this error is generated. ].join(" | ");//} let additionals = { Format:".//h3", Pairs:{ field:".//div[contains(concat(' ',@class,' '), ' format_info_left ')]", value:"following-sibling::div[contains(concat(' ',@class,' '), ' format_info_right ')][1]"}, szDuration:{script:"szDuration", field:"duration", testNull:false}, // Subtitle:".//div[@id = 'subtitle']", }; */ obj.getAdditional({ /*SectionsPair:{field:"("+titles.map(function(n){return "descendant::"+n;}).join(" | ")+")", end:"", value:function(r){ var m = doWhile(r, expr); var n = m.slice(1); var s = m[0] n.forEach(function(v, i){ insertAfter(v, m[i]); }); return n;//.reduce(function(array, node){ return array.concat(node.nodeName == "B"?Array.slice(node.children):[node]); }, []); }},*/ Formats:".//ul["+xPathListContains("@class", "formats-at-download")+"]", TitleInfo:{field:".//div[@id='formatInfoExpand']/ul/li/div[1]/div[1]/h6[1]", value:function(t, i, obj, name, alt, results){ let title = t.textContent.trim(); let r = $X("ancestor::li[1]", t); switch(title) { case "Publisher":{ return $X("./div[2]/div[1]", r); } } var f = new FormatItem(obj, [r]); let fn = Formats.NameLookup(title); if(fn) f.formats.push(fn); f.getAdditional({ "Pairs":{field:"./div[count(div)>1]/div[1]", value:"./following-sibling::div[1]"}, szFormatType:{script:"szFormatType", number:"format", testNull:false}, }); if(f.format){ if(Array.isArray(obj.formats)){ a = obj.formats; obj.formats = b = {}; for(n of a) b[n] = undefined; } obj.formats[f.format] = f; return;//We have parsed it. } //values returned by this function result in the pair get parsed. return $W("./div[position()>1]", r);//pretty sure I can do this. }, end:""}, szEdition:{script:"szEdition", field:"edition", testNull:false}, szSeries:{script:"szSeries", field:"series", testNull:false}, szImprint:{script:"szImprint", field:"imprint", testNull:false}, //Pairs:{field:"ancestor::td[1]//tr/td[1][b]", value:"../td[2]", node:"szImprint-script"}, Title:{xpath:".//div[@id='detailsTitle']/h3"}, //Edition:{xpath:"ancestor::div[1]/following-sibling::div[starts-with(text(),'Edition: ')]", node:"Title-node", params:[9]}, //Series:{xpath:"ancestor::div[1]/following-sibling::div[text()='Series: ']", node:"Title-node"}, Subtitle:".//div[@id = 'subtitleDetails']/h6", Creators:{xpath:".//div[@id='creatorDetails']/h5/a | .//div[@id='creatorSubDetails']/ul/li/h6/a", parse:function(a, i, obj, name, alt, results){ //results.push(r); if(!obj.creatorObjs) obj.creatorObjs = []; let name = a.textContent.trim(); let value = SplitTextNode(findTextElementContaining(name, a), name); let num = search2obj(a.search, true).id; obj.creatorObjs.push(find_id.exec(num)?{name:name, id:num, link:a, text:value}:{name:name, number:num, link:a, text:value}); return a; }}, Image:".//a[starts-with(@href,'ContentDetails-Cover.htm?')]/img", //Sample:{xpath:"../following-sibling::div/a[starts-with(@href,'http://excerpts.contentreserve.com/') and img]", node:"Image-node"}, //Subjects:".//div[@id = 'subject']/a", //Publisher:".//div[@id = 'publisher']/text()[last()]", //Language:".//div[@id = 'language']/text()[last()]", "Recommended for you":{xpath:".//div[@id='crossMarketing']//ul/li", parse:function(r){ var f = new Item([r]); f.getAdditional({ Title:"./div[contains(concat(' ',@class,' '), ' trunc-title-line ')]", Image:"./div[contains(concat(' ',@class,' '), ' coverID ')]/a/img", Author:"./div[contains(concat(' ',@class,' '), ' trunc-author-line ')]", }); (obj.suggestions = (obj.suggestions || [])).push(f); // sp.linkify(f); // delete f.formats; return r; }}, }); //These pages are really screwed up and hard to parse. //imss.lib.overdrive.com lacks a "Release date"! I thought it was universal! for(let co of obj.creatorObjs){ let span = OverDrive.createServiceLinks(insertAfter(document.createElement("span"), co.link), {creator:co.name, creatorNumber:co.number}, "creator"); span.className = "links"; LinkAdjust(span, null, null, false);//this is a terrible hack!!! } if(obj.nodes["Series-node"]){ let span = OverDrive.createServiceLinks(obj.nodes["Series-node"].appendChild(document.createElement("span")), obj, "series"); span.className = "links"; //LinkAdjust(span, null, null, false);//this is a terrible hack!!! } if(obj.nodes["Title-node"]){ let span = OverDrive.createServiceLinks(obj.nodes["Title-node"].appendChild(document.createElement("span")), obj, "title"); span.className = "links"; } if(obj.nodes["Publisher-node"]){ let span = OverDrive.createServiceLinks(obj.nodes["Publisher-node"].appendChild(document.createElement("span")), obj, "publisher"); span.className = "links"; } /* let cdgo = function (r){// or starts-with(text(),'Available copies:') var stable = r; var script = null; let fobj = new FormatItem(obj); if(version){ if(!fobj.addPieces([r])) return; //already used, so skip it } else{ if(!fobj.addPieces([r.parentNode])) return; //already used, so skip it do stable = $X(compileXPath("ancestor::table[1]"), stable); while(stable && !(script = $X(compileXPath(".//script[contains(text(), 'var szFormatType = ') and not(@parsed)] | preceding-sibling::table//script[contains(text(), 'var szFormatType = ') and not(@parsed)]"), stable))); var mark; if(script) fobj.formats.push(Number(script.textContent.match(/var szFormatType = "([^"]*)";/)[1])); } // fobj.id = obj.id; // fobj.creator = obj.creator; if(!fobj.format){//HACK!!! fobj.getAdditional({ szFormatType:{script:"szFormatType", field:"format", testNull:false}, SmartLink:{xpath:".//a", params:["format"]}, }); } var szFormatType = fobj.format if(szFormatType){ //log(szFormatType) $Z(compileXPath(".//script[contains(text(), 'var szFormatType = \""+szFormatType+"\"') and not(contains(concat('~',@parsed,'~'), '~"+szFormatType+"~'))]"), function(rr){ fobj.setNodeParsed(rr, szFormatType); fobj.addPieces("ancestor::table[1]/tbody", rr); fobj.addPieces("ancestor::table[1]/parent::td[count(*) = 1]/parent::tr", rr); }, base); $Z(compileXPath(".//b/text()[(contains(.,\""+Formats.Numbers[szFormatType].names.join("\") or contains(.,\"")+"\"))]"), function(rr){ insertAsParent(OverDrive.Services.library.createLink("title", fobj), rr); fobj.addPieces("ancestor::tbody[1]", rr); fobj.addPieces("ancestor::table[1]/parent::td[count(*) = 1]/parent::tr", rr); }, base); } else log("format not detected", obj); fobj.getAdditional(additionals); fobj.enhance(); sp.linkify(fobj); if(szFormatType) obj.formats[szFormatType] = fobj.clone( {unique:null, formats:null}, true ); let newrow = (r.cells)?function(text, className){ let tr = document.createElement("tr"); if(className) tr.className = className; for(let empties = r.cells.length - 2;empties > 0; empties--) tr.appendChild(document.createElement("td")); for each(var a in Array.slice(r.attributes)) tr.setAttribute(a.name, a.value); tr.classList.add("others"); let n = tr.appendChild(document.createElement("td")); n.appendChild(document.createTextNode(text + "\u00a0")); n.setAttribute("nowrap","nowrap"); insertAfter(tr,r); return tr.appendChild(document.createElement("td")); }:function(text, className){ let n = (document.createElement("div")); n.appendChild(document.createTextNode(text + "\u00a0")); n.setAttribute("nowrap","nowrap"); n.className="format_info_left" let m = (document.createElement("div")); m.className="format_info_right"; //insert after the last row we parsed with pairs insertAfter(n, fobj.nodes[fobj.nodes["Pairs-pairs"].filter(function(a){return a[0];}).slice(-1)[0][1]+"-node"]); insertAfter(m, n); return m }; {//Other Libraries & Stores let library, store; for each(let d in Providers.activeProviders) { if(d != Server) { let p = Providers.getProvider(d); let k = p.data; if(k)// && k.url) { var a = document.createElement("a"); a.href = "http://"+ d + "/ContentDetails.htm"+location.search; a.appendChild(document.createTextNode(k.title || d)); switch(k.type) { case "store":{ if(store == null) store = newrow("Other Stores:\x0a\x0a", "links") store.appendChild(a); store.appendChild(document.createElement("br")); break;} default: case "library":{ if(library == null) library = newrow("Other Libraries:\x0a\x0a", "links"); library.appendChild(a); let span = library.appendChild(document.createElement("span")) let last = span.appendChild(document.createTextNode(" [")); span.className = "right" for each(let name in ["WishListAdd", "WishListRemove", "series","creator"]){ let link = p.createLink(name, fobj, "abbr") if(link){ span.appendChild(link) span.appendChild(last = document.createTextNode(", ")); p.convertLink(link); } } if(last.nodeValue == ", ") last.nodeValue = "]"; else remove(last); library.appendChild(document.createElement("br")); break;} } } } } } {//Databases let td = newrow("Databases:\x0a\x0a", "links"); let last; for each(let service in OverDrive.Services) if(service.service != server_mode) {//I don't think this is ever false, but I don't know all the ways of getting in here. td.appendChild(service.createLink("title", fobj, document.createTextNode(service.abbreviation))); let s = service.createLink("title-search", fobj, document.createTextNode("S")); if(s){//this is likely dead code since title-search is temporarily dead. let span = td.appendChild(document.createElement("span")) span.className = "sup" span.appendChild(last = document.createTextNode("(")); span.appendChild(s); span.appendChild(last = document.createTextNode(")")); } td.appendChild(last = document.createTextNode(" ")); } else log("contentdetails gets called in databases", server_mode, page); if(last) remove(last); } } $Z(".//tr[td[starts-with(text(),'Release date:')]]", cdgo, base);//'Fecha de Publicación:' or 'On sale date:' //$Z(".//tr[td[starts-with(text(),'Tamaño del archivo:')]]", cdgo, base);//File size: $Z(".//tr[td[starts-with(text(),'ISBN:')]]", cdgo, base); $Z(".//div[@id=\"format_info\"]/div[contains(concat(' ',@class,' '), ' format_container ')]", cdgo, base); */ obj.enhance(); sp.linkify(obj); // dirty = obj.saveCopies(server_obj); log(obj); break; } case "waitinglistremove": case "waitinglistconfirm":{ if(!error && signed_in) sp.update_page(page, "waitinglist"); title_link = "MyWaitingList"; /*Todo Can get hold duration from this page! WaitingListConfirm.htm */ break; } case "waitinglistedit": case "waitinglistform":{ if(!error && signed_in) { //var form = $X("//form[@action='BANGAuthenticate.dll?Action=LibraryWaitingList' or @action='BANGAuthenticate.dll?Action=EditWaitingList']"); var obj = new Item(); obj.getAdditional({ LibraryWaitingList:{form:"//form[@action='BANGAuthenticate.dll?Action=LibraryWaitingList' or @action='BANGAuthenticate.dll?Action=EditWaitingList']",}, Pairs:{field:"../tbody/tr/td[1]", value:"following-sibling::td[last()]/*[self::b or self::strong]", node:"LibraryWaitingList-form"}, DeComment:{xpath:"../tbody/comment()", parse:function(r){ (evalNoScript(r, null, ["table", "tbody"], true), arguments)}, node:"LibraryWaitingList-form"}, // Subtitle:{xpath:".[contains(.//text(), ': ')]", node:"Title-value"},//no subtitle if there is no splitter. }); if(obj.title){ let s = obj.title.split(": "); if(s.length > 1 && obj.title.match(obj.subtitleParser)) { obj.getAdditional({Subtitle:obj.nodes["Title-value"], });//no subtitle if there is no splitter. var i = Math.ceil(s.length / 2); obj.title = s.slice(0, i).join(": "); obj.subtitle = s.slice(i).join(": "); } } //I do not recall why enhance is not called obj.processSubtitle(); sp.linkify(obj); log(obj); } title_link = "MyWaitingList"; break; } case "mycompletewishlistgrid": case "mycompletewishlistlist": case "mycompletewishlistcover"://only in child layout case "mycompletewishlist": case "myrecformegrid"://only in child layout case "myrecformelist"://only in child layout case "myrecforme"://only in child layout case "searchresultsgrid": case "searchresultslist": case "searchresults":{ let page_base = /^(myrecforme|mycompletewishlist|searchresults)(.*)$/.exec(page) || ""; let base; if(page_base){ [,page_base, page_mode] = page_base; if(page_mode) page_mode = "-"+page_mode; let t = {/*myrecforme:[], */mycompletewishlist:["@id='myAccount'", "@id='option1' and parent::div[@id='myAccountContainer']"], searchresults:["@id='searchResultsContainer'", "@id='searchResults'"]}[page_base] if(t) base = $X("//section["+t[0]+"]//div["+t[1]+"]"); else log("page type not handled!") } //var items = []; /* GM_addStyle([//{ "tr.item > td { width:auto; white-space:nowrap; }",// default table spacing is bad "[parsed], tr.item img, .binlink { white-space:normal!important; }",//if we parsed it, best to let it wrap... or if it's an image "a.checked[parsed] { white-space: nowrap!important; }", ].join("\n"))//}*/ //Library Bin not supported yet, this needs to be entirely overhauled. //instead of cheating we need to actually try to parse the rows ~_~ //let base = $X("//section[@id='searchResultsContainer' or @id='myAccount']//div[@id='searchResults' or (@id='option1' and parent::div[@id='myAccountContainer'])]"); let countNodes = []; let countStart = 0; $Z("//div[@id='pagingTitleCount']/text()", function(r){ let s = /([1-9]\d*)\s*-\s*([1-9]\d*)\s+of\s+([1-9]\d*)/.exec(r.nodeValue); if(s){ //objFilter(s, {"start":1, "end":2, "total":3}, countNodes); [,countNodes.start, countNodes.end, countNodes.total] = s;//not as elegant but functionaly the same and it's faster. let t = SplitTextNode(r, countNodes.end); if(t) countNodes.push(t); } }); // log(countNodes) let script = $X("//script[contains(text(),'// ADD TO WISHLIST SCRIPT')]") if(script){ let text = script.innerHTML; //HACK HACK HACK let start = text.indexOf("// ADD TO WISHLIST SCRIPT"); let end = text.indexOf("// CLOSE ALL EXPANDED DETAIL BOXES FOR SCREEN SIZES"); if(start < 0 || end < 0) { log("AtWS parsing failed"); script = null; } else { let rscript = document.createElement("script"); rscript.type=script.type; rscript.innerHTML = "var redoWishlist = (function(nody){" + text.substring(start,end).replace('$(".wishlist-link")','$(nody).find(".wishlist-link")',"g") + "});"; rscript.id = "GME" insertAfter(rscript, script); } } else { log("Failed to find AtWS"); } let objs = ListPage(insertBefore(document.createElement("div"), base), {pagingCleanup:script?function(objs){ for(let obj of objs){ unsafeWindow.redoWishlist((obj));//not ideal but ok. } }:undefined, paging:true, ready:true, filter:true, countNodes: countNodes, sample:page_mode != "", formatGroup:true}, $X("./div[not(@id)]/div[not(@id)]/ul", base), "./li | .//li[contains(concat(' ',@class,' '), ' search-result"+page_mode+" ')]", [function(r, i){ let obj = new Item([r]); let node = r; //resultsBorrowButton //szPrimaryCreatorID //log(r); let proc2 = function(t){ switch(t){ case "Borrow": obj.status = "ready"; break; case "Place a Hold": obj.status = "wait"; break; case "Recommended": obj.status = "recommended"; break; case "Recommend": obj.status = "recommend"; break; case "Not Available": obj.status = "unavailable"; break; } } let proc = function(t){ proc2(t.textContent.trim()) } obj.getAdditional({ //This only works if the button has a format field, which it won't always have. Only the hold buttons still have the format numbers, it's been stripped from borrow. //recommendHolder:{xpath:".//div[@class='recommend-holder']", parse:function(t){ obj.status = "unavailable"; }}, Available:{xpath:".//a[@id='resultsBorrowButton']", parse:proc}, FormatFinder:{xpath:".//noscript[@class='noscript-borrow-button']", parse:function(t){ let a = $X(".//a", evalNoScript(t, document.createElement("div"))); if(!obj.status) { proc(a); }//somethings don't have a status! let o = search2obj(a.search, true); if(!o.format) { if(!o.url) return; a.href = o.url; o = search2obj(a.search, true); } if(o.format && Formats.Numbers[o.format]) obj.format = Number(o.format); }, }, formatGroup:{xpath:".//@data-fmtid", params:["format".length]}, Description:".//div[contains(concat(' ',@class,' '), ' trunc-desc"+page_mode+" ')]", Title:".//div[contains(concat(' ',@class,' '), ' trunc-title-line"+page_mode+" ')]", Subtitle:".//div[contains(concat(' ',@class,' '), ' trunc-subtitle-line"+page_mode+" ')]", // Series:".//div[contains(concat(' ',@class,' '), ' trunc-series-line"+page_mode+" ')]", Author:".//div[contains(concat(' ',@class,' '), ' trunc-author-line"+page_mode+" ')]", // "Release date":{xpath:"ancestor::td[1]/div/small[starts-with(text(), 'Release date: ')]", node:"Title-node", params:[14]}, // PreRelease:{xpath:".//a[text()='Pre-Release Title']", parse:function(r, i, obj){obj.status = "unavailable";}}, Image:".//div[contains(concat(' ',@class,' '), ' coverID ')]/a/img", Available:{xpath:".//*[@data-izavail]", parse:function(node, i, obj, name, inst, results){ let value = Number(node.getAttribute("data-izavail").substr(6)); if(!isNaN(value)){ if(value == 0) obj.status = "wait"; else if(value > 0) obj.status = "ready"; else obj.status = "unavailable"; } return node; }}, // Sample:{xpath:"../following-sibling::div/a[starts-with(@href,'http://excerpts.contentreserve.com/') and img]", node:"Image-node"}, // "Available copies":{xpath:"following-sibling::div[1]", node:"Description-node"}, // "Library copies":{xpath:"following-sibling::div[2]", node:"Description-node"}, szSeries:{script:"szSeries", field:"series", testNull:false}, // szLibOwned:{script:"szLibOwned", parse:function(vn, v){ if(!Number(v)){ obj.status = "unavailable"; }}}, // szLibCopies:{script:"szLibCopies", parse:function(vn, v){ obj.libraryCopies = (v == 'always available')?Number.POSITIVE_INFINITY:Number(v)}}, // szCopies:{script:"szCopies", parse:function(vn, v){ obj.availableCopies = (v == 'always available')?Number.POSITIVE_INFINITY:Number(v)}}, // szNumWaiting:{script:"szNumWaiting", parse:function(vn, v){ obj.waiting = Number(v)}}, // WishList:{script:"szAdvInPrivate", parse:function(vname, value, obj, node, name, alt){ // //FIXME: I do not know what this was supposed to do but it breaks things. // if(Number(value) && !signed_in) // evalNoScript($X("following-sibling::noscript[1]", node)); // }}, WishList:".//div[contains(concat(' ',@class,' '), ' coverID ')]/div[contains(concat(' ',@class,' '), ' wishlist-container ')]/a", Recommend:".//div[contains(concat(' ',@class,' '), ' coverID ')]/div[contains(concat(' ',@class,' '), ' rtl-container ')]",//this picks up all sorts of random shit! }); // log(server_obj) let test = (server_obj && server_obj.formats)? function(e){return (obj.formats.indexOf(e) == -1) && (server_obj.formats.indexOf(e) != -1);}: function(){return obj.formats.indexOf(e) == -1;}; for each(let f in obj.formats){ for each(var c in Formats.Classes) if(c.title == f){ for each(let e in c.formats) if(test(e)) obj.formats.push(e); obj.formats.splice(obj.formats.indexOf(f), 1); } } // log() obj.enhance(); sp.linkify(obj); // dirty = obj.saveCopies(server_obj) || dirty; // if(obj.nodes["Title-node"]) // obj.nodes["Title-node"].appendChild(OverDrive.createServiceLinks(document.createElement("span"), obj)).className = "links"; return obj; }], imageResizing); log(objs); break; } case "searchresultsrecommend": ++page_mode; case "searchresultsuserrank": ++page_mode;//Active as of Dec 10, 2013 {//case "searchresults": //let base = $X("//section[@id='searchResultsContainer']/div[@id='searchResults']"); //let objs = ListPage(insertBefore(document.createElement("div"), base), {paging:true, ready:true, filter:true, countNodes: countNodes, sample:true}, base, "/html/body/div[5]/div/div/section/section/div[4]/div[2]/div/ul", function(r, i){ //var items = []; //* GM_addStyle([//{ "tr.item > td { width:auto; white-space:nowrap; }",// default table spacing is bad "[parsed], tr.item img, .binlink { white-space:normal!important; }",//if we parsed it, best to let it wrap... or if it's an image "a.checked[parsed] { white-space: nowrap!important; }", ].join("\n"))//} //Library Bin not supported yet, this needs to be entirely overhauled. //instead of cheating we need to actually try to parse the rows ~_~ let base = $X("//table[script[contains(text(),'g_bFutureOnSaleDate')]/following-sibling::tbody[position()=1]] | //script[contains(text(), ' szPrimaryCreatorID ')]/ancestor::table[1]"); let xpaths; if(base){//legacy ~_~ xpaths = { count:"//tr[not(.//table) and td[.//text()[contains(., '< Previous')] and .//text()[contains(., 'Next >')]]]/td//text()[contains(., 'Showing')]", list:"tbody/tr[td[@rowspan]]", group:"following-sibling::node()[not(self::text())][position()=1 and not(td[@rowspan])]", Description:"td/small", Title:".//b[a[starts-with(@href, 'ContentDetails.htm?') and string-length(text())>1]]", Subtitle:"ancestor::td[1]/div/small/b[text()]", Image:".//td/a[@href]/img", Pairs:{field:".//tbody[tr[1]/td[1][starts-with(text(), 'Title:')]]/tr/td[1]", value:"ancestor-or-self::td[1]/following-sibling::td[1]"}, }; } else if(base = $X("//div[@id='results_page_top']")){ xpaths = { count:"//div[@id='results_showing']/text()", list:"ancestor::div[position() < 3]//div[contains(concat(' ',@class,' '), ' results_ratings ') or contains(concat(' ',@class,' '), ' results_holder ')]", group:"following-sibling::node()[not(self::text())][position()=1 and not(self::div[contains(concat(' ',@class,' '), ' results_ratings ')])]", Description:".//div[contains(concat(' ',@class,' '), ' results_description ')]", Title:".//div[contains(concat(' ',@class,' '), ' results_title ')]", Subtitle:"following-sibling::div[contains(concat(' ',@class,' '), ' results_sub_title ')]", Image:".//div[contains(concat(' ',@class,' '), ' results_cover_img ')]/a[@href]/img", Pairs:{field:".//b", value:"self::b[count(following-sibling::node()) > 1]/* | self::b[count(following-sibling::node()) = 1]/following-sibling::node()"}, }; } let countNodes = []; let countStart = 0; $Z(xpaths.count, function(r){ objFilter(/Showing\s+([1-9]\d*)\s*-\s*([1-9]\d*)\s+of\s+([1-9]\d*)/.exec(r.nodeValue), {"start":1, "end":2, "total":3}, countNodes); countNodes.push(SplitTextNode(r, countNodes.end)); }); // log(countNodes) let objs = ListPage(null, {paging:true, ready:true, filter:true, countNodes: countNodes, sample:true}, base, xpaths.list, function(r, i){ let obj = new Item(); let node = r; let realTRs = 0; capture: do{ switch(node.nodeName) { case "DIV": if($X("a/img[@alt=\"Don't see the titles you were hoping to find? Find More\"]", node)) break capture; if(!obj.addPieces(node)) return; break; case "TR": obj.addPieces(node); ++realTRs; break; case "#comment"://evaluate the comments, sometimes they contain interesting stuff! obj.addPieces(evalNoScript(node.textContent.trim(), undefined, ["table", "tbody"]).filter(function(f){return f.nodeName!="#text";})); break; } }while(node = $X(xpaths.group, node)); //cleanup for bad websites! (needed occasionally ~_~) if(r.cells){ for(node = obj.pieces.slice(-1)[0]; realTRs < r.cells[0].rowSpan; realTRs++) { let s = insertAfter(document.createElement("tr"), node) s.appendChild(document.createElement("td")).appendChild(document.createElement("hr")); obj.addPieces(s); } } //szPrimaryCreatorID //log(r); if($X("td[3]/font[@color='red']", r)) obj.status = "unavailable"; else//this method is less than perfect obj.status = obj.pieces.some(function(n){return $X(".//a/@href[starts-with(., 'BANGCart.dll?Action=Add&')]",n)})?"ready":"wait"; obj.getAdditional({ Pairs:xpaths.Pairs, FormatFinder:{xpath:".//text()[string-length(.)>5]", parse:function(t){ let f = Formats.Names[t.nodeValue.trim()]; if(f) obj.format = f; }, }, SmartLink:{xpath:".//a", all:true, params:["id","format"]}, szPrimaryCreatorID:{script:"szPrimaryCreatorID", parse:function(vname, value, obj, node, name, alt){ if(value) { let go = function(){ obj.getAdditional({ Creator:{xpath:"../text()[last()][not(translate(.,' \t\n','')='')]"}, Author:{xpath:"following-sibling::tr[1]/td[1][starts-with(text(), 'Author:')]/following-sibling::td[1]"}, }, node); if(obj.creatorObjs && obj.creatorObjs.length) { //X_X obj.creatorObjs[0][find_id.exec(value)?"id":"number"] = value; return false; } return true; } if(go()){ //oh hell there is an error in the string generating the node x_x //It's too much work to try and fix it. log("bad szPrimaryCreatorID script"); scriptReEval(node, node.innerHTML.replace(/\n([^\s])/g, "$1"), true); if(go())//still failed obj.creatorObjs = [{id:value}]; } } }}, Description:xpaths.Description, Title:xpaths.Title, Subtitle:{xpath:xpaths.Subtitle, node:"Title-node"}, Author:{xpath:"ancestor::td[1]/div[starts-with(text()[1], 'by ')]", node:"Title-node", params:[3]},// | following-sibling::div[contains(concat(' ',@class,' '), ' results_creator ')]/small/text()[last()] "Release date":{xpath:"ancestor::td[1]/div/small[starts-with(text(), 'Release date: ')]", node:"Title-node", params:[14]}, PreRelease:{xpath:".//a[text()='Pre-Release Title']", parse:function(r, i, obj){obj.status = "unavailable";}}, Image:xpaths.Image, Sample:{xpath:"../following-sibling::div/a[starts-with(@href,'http://excerpts.contentreserve.com/') and img]", node:"Image-node"}, // "Available copies":{xpath:"following-sibling::div[1]", node:"Description-node"}, // "Library copies":{xpath:"following-sibling::div[2]", node:"Description-node"}, szLibOwned:{script:"szLibOwned", parse:function(vn, v){ if(!Number(v)){ obj.status = "unavailable"; }}}, szLibCopies:{script:"szLibCopies", parse:function(vn, v){ obj.libraryCopies = (v == 'always available')?Number.POSITIVE_INFINITY:Number(v)}}, szCopies:{script:"szCopies", parse:function(vn, v){ obj.availableCopies = (v == 'always available')?Number.POSITIVE_INFINITY:Number(v)}}, szNumWaiting:{script:"szNumWaiting", parse:function(vn, v){ obj.waiting = Number(v)}}, WishList:{script:"szAdvInPrivate", parse:function(vname, value, obj, node, name, alt){ //FIXME: I do not know what this was supposed to do but it breaks things. // if(Number(value) && !signed_in) // evalNoScript($X("following-sibling::noscript[1]", node)); }}, }); // log(server_obj) let test = (server_obj && server_obj.formats)? function(e){return (obj.formats.indexOf(e) == -1) && (server_obj.formats.indexOf(e) != -1);}: function(){return obj.formats.indexOf(e) == -1;}; for each(let f in obj.formats){ for each(var c in Formats.Classes) if(c.title == f){ for each(let e in c.formats) if(test(e)) obj.formats.push(e); obj.formats.splice(obj.formats.indexOf(f), 1); } } // log() obj.enhance(); sp.linkify(obj); // dirty = obj.saveCopies(server_obj) || dirty; if(obj.nodes["Title-node"]) obj.nodes["Title-node"].appendChild(OverDrive.createServiceLinks(document.createElement("span"), obj)).className = "links"; return obj; }, imageResizing); //*/ log(objs); break; } case "advancedsearch":{ GM_addStyle("div#advSearchCollectionsRTL.row { display:block !important; }"); let go = function(select, i, get_value, compare, wants, make, sort, def) { //this method sucks but the only way to compare is by value but sorting may be by value or by text compare = compare || function(a,b){return a === b;}; log(Array.slice(select.options). map(function(o){ let v = get_value(o); wants = wants.filter(function(u){return !compare(u,v);}); return {value:v, option:select.removeChild(o)}; })). concat(log(wants).map(function(v){let o = document.createElement("option"); make(o, v); return {value:v, option:o};})). sort(sort || function(a, b){return a.value - b.value;}). map(function(o){select.appendChild(o.option); return o;}). some(def?function(o){return compare(o.value, def)?o.option.selected = true:false;}:function(){return true;}); } let counts = [5,10,25,50,100,200,300,500,1000];//more than 100 per page is slow. if(counts.indexOf(QuickSearchsCount) == -1) counts.push(QuickSearchsCount); $Z("//select[@name='CollDate']", go, null, function(f){return (f.value && Number(f.value.slice(2))) || 0;}, null, DaysOfInterest, function(o, days){ o.value = '>*'+days; o.appendChild(document.createTextNode("Within the last ")); o.appendChild(document.createTextNode(days)); o.appendChild(document.createTextNode(" days")); }, null, QuickSearchsDays); $Z("//select[@name='PerPage']", go, null, function(f){return (f.value && Number(f.value)) || 0;}, null, counts, function(o, v){ o.appendChild(document.createTextNode((o.value = v)>100?v+" (bad idea)":v)); }, null, QuickSearchsCount); /* //evilFormatDivider var groups = []; let test; if(server_obj.formats && server_obj.formats.length) test = function(f){ return server_obj.formats.indexOf(f) > -1; }; else{ test = function(f){return $X(compileXPath("//select[@name='Format']//option[@value='"+f+"']")); };//hack! //what a pain in the ass! if(server_obj){ let formats = []; for each(var cls in Formats.Classes) formats = formats.concat(cls.formats.filter(test)); if(formats.length){ server_obj.formats = formats; server_obj.formats_load = new Date(document.lastModified).getTime(); dirty = true; } } } for each(var cls in Formats.Classes) { let formats = test?cls.formats.filter(test):cls.formats.slice(); if(formats.length > 0) { formats.title = cls.title + " Formats"; groups.push(formats); } } // log(groups) $Z("//select[@name='Format']", go, null, function(f){//get_value return (f.value && splitFormats(f.value).map(Number).sort(function(a, b){return a - b;})) || [0];//hack so it sorts }, function(want, have) {//return true if everything in want is in have for (let i = 0, j = 0; i < want.length; i++) { if(j == have.length) return false; let w = want[i]; while(w > have[j]) if(++j == have.length) return false; if(w !== have[j++]) return false; } return true; }, groups, function(o, v){//make o.value = joinFormats(v); o.appendChild(document.createTextNode(v.title)); }, function(a, b){//sort if(!a.option.value) return -1; else if(!b.option.value) return 1; else return (a.option.textContent > b.option.textContent) - (a.option.textContent < b.option.textContent); }); //*/ log(server_obj) break; } case "browsesubjects": case "browsevideo": case "browseebooks": case "browsesubject1": case "default":{ if(server_obj) server_obj.title = document.title; dirty = true; /*var urls = [ "http://images.contentreserve.com/ImageType-200/", ];*/ //img[starts-with(@src, 'http://images.contentreserve.com/ImageType-200/')]/ancestor::*[self::div[contains(concat(' ', @class, ' '), ' title-grid-item ')] or self::td][1] var objs = []; $Z("//a[img[starts-with(@data-original, '//images.contentreserve.com/ImageType-') or @data-original='MissingThumbImage.jpg']]", function(a){ var base = $X(compileXPath("ancestor::*[self::div[contains(concat(' ', @class, ' '), ' title-grid-item ')] or self::td or self::li][1]"), a); //log(a, base); if(base) { var obj = new Item([base]); obj.getAdditional({ Image:a, Title:{xpath:"following-sibling::b | ../self::div/following-sibling::div[1]", node:"Image-node"}, Creator:"div[position()>2]//text()[not(ancestor::a)]",//needs tweaking. Author:"text()[contains(., 'by ')]/following-sibling::*[self::a]", Enhanced:{xpath:"ancestor::div[1]/preceding-sibling::div[1]/small", node:"Creator-value", parse:function(){}}, }); obj.enhance(); sp.linkify(obj); if(obj.nodes["Title-node"]) obj.nodes["Title-node"].appendChild(OverDrive.createServiceLinks(document.createElement("span"), obj, "title")).className = "links";//"title-search" objs.push(obj); } }); GM_addStyle([//{ ".trunc-title-line a { display:inline; }", ".trunc-title-line { white-space: normal; }", ".trunc-title-line span.links { display: inline-block; margin-left: 0;}", ".trunc-title-line span.links a { display: inline; }", ].join("\n")).id="defaultMangler";//} log(objs); break; } case "odadvantage":{ if(!error && server_obj) { let advantage = []; $Z(".//p[text()='The following libraries are members of OverDrive Advantage:']/following-sibling::table/tbody/tr/td[2]", function(r,i){ advantage.push(r.textContent); }, main); //server_obj.advantage = advantage; //dirty = true; } break; } case "myaccount":{ signed_in = !error; //TODO: add options to settings tab, including overriding the value of server_obj.title (aka data.title) for links. if(!error) {//MyAccount.htm?TitleLimit=0 //bookshelfBlockGrid let base = $X("//section[@id='myAccountContent']/ul[contains(concat(' ',@class,' '), ' tabs-content ')]"); //When we convert our new links from javascript links to forms, if the tab is hidden the result is the submit will be the wrong size. let config = { attributes: true, /**/attributeFilter: ["class"],/**/ }; $Z(compileXPath("./li"), function(r){ let observer; observer = new MutationObserver(function(mutations) { for(let m of mutations) { let rr = m.target; if(rr.className){ sp.resizeLinkCheck(rr); observer.disconnect(); } } }); observer.observe(r, config); }, base) let features = {paging:false, ready:true, filter:true, sample:false, series:true, formatGroup:true}; {//bookshelf let bookshelf = ListPage(null, objCreate(features, {root:"#myAccount1Tab"}), $X("./li[@id='myAccount1Tab']//ul[@id='bookshelfBlockGrid']", base), "./li", function(r, i){ let obj = new Item([r]); obj.getAdditional({ Author:".//div[contains(concat(' ',@class,' '), ' trunc-author-line ')]", Title:".//div[contains(concat(' ',@class,' '), ' trunc-title-line ')]", Image:".//div[contains(concat(' ',@class,' '), ' coverID ')]/a/img", TitleFallback:{xpath:"./div/@title", parse:function(node, i, obj, name, inst, results){ if(!obj.title){ results.push(node); obj.title = node.value; return obj.nodes["Title-value"] = SplitTextNode(findTextElementContaining(obj.title, r), obj.title); } } }, /**/ Formats:".//ul[li//@data-fmtid]", Read:{xpath: ".//div[" +xPathListContains("@class", "dwnld-odread")+ " and *]", parse:()=>obj.formats.push(610)}, Format:".//@data-lckdinfmtid[not(.='-1')]", nLockFormat:{script:"nLockFormat", parse:function(vname, value, obj, node, name, alt){let f = Number(value); if(f != -1 && obj.Formats.indexOf(f) == -1) f.formats.push(f);},}, Download:".//a[span["+xPathListContains("@class", "read-button-text-download")+" or "+xPathListContains("@class", "dwnld-span-bottom")+"]]", /**/ myExpireDate:{script:"myExpireDate", /**/field:"expires",/*/ parse:function(vn, v){ obj.expires = Date(v)},/**/}, formatGroup:".//@data-svenh", ID:".//@data-reserveid", Sample:".//@data-cerpturl", SampleFormat:".//@data-excerpt", }); if(typeof obj.title === "undefined") return; obj.enhance(); sp.linkify(obj); return obj; }); } {//holds let holds = ListPage(null, objCreate(features, {root:"#myAccount2Tab"}), $X("./li[@id='myAccount2Tab']//ul[contains(concat(' ',@class,' '), ' block-grid ')]", base), "./li", function(r, i){ let obj = new Item([r]); obj.getAdditional({ formatGroup:".//@data-svenh", Borrow:{xpath:".//li[contains(concat(' ',@class,' '), ' holds-borrow-link ')]", parse:function(){obj.status="ready"}}, Author:".//div[contains(concat(' ',@class,' '), ' trunc-author-line ')]", Title:".//div[contains(concat(' ',@class,' '), ' trunc-title-line ')]", Image:".//div[contains(concat(' ',@class,' '), ' coverID ')]/a/img", "Release date":{xpath:".//div[contains(concat(' ',@class,' '), ' holds-info ')]/span["+xPathListContains("@style","color:red",";")+"]", parse:function(r,i){ this["release-date"] = r.textContent.slice(-12); obj.status = "unavailable"; }}, //Format? //myExpireDate:{script:"myExpireDate", /**/field:"expires",/*/ parse:function(vn, v){ obj.expires = Date(v)},/**/}, //user 7 of 57, library copies //available }); if(!obj.status) obj.status = "wait"; obj.enhance(); sp.linkify(obj); return obj; }); } {//lists let lists = ListPage(null, objCreate(features, {filter:false, root:"#myAccount3Tab"}), $X("./li[@id='myAccount3Tab']//ul[contains(concat(' ',@class,' '), ' block-grid ')]", base), "./li", function(r, i){ let obj = new Item([r]); obj.getAdditional({ formatGroup:".//@data-fmtclass", Status:{xpath:".//@data-izavail", parse:function(node){ let t = node.value.slice(6); if(t < 0) obj.status= "unavailable"; else if(t == 0) obj.status= "wait"; else if(t > 0) obj.status= "ready"; else obj.status= "unknown"; } }, Author:".//div[contains(concat(' ',@class,' '), ' trunc-author-line ')]", Title:".//div[contains(concat(' ',@class,' '), ' trunc-title-line ')]", Image:".//div[contains(concat(' ',@class,' '), ' coverID ')]/a/img", /*"Release date":{xpath:".//div[contains(concat(' ',@class,' '), ' holds-info ')]/span["+xPathListContains("@style","color:red",";")+"]", parse:function(r,i){ this["release-date"] = r.textContent.slice(-12); obj.status = "unavailable"; }},*/ //Format? //myExpireDate:{script:"myExpireDate", /**/field:"expires",/*/ parse:function(vn, v){ obj.expires = Date(v)},/**/}, //user 7 of 57, library copies //available }); if(!obj.status) obj.status = "wait"; obj.enhance(); sp.linkify(obj); return obj; }); } } break; } default:{ done = false; }//{ } if(!done) {//} if(page.match(/^(?:log|sign)in/)) {//signin and login pages done = true; } } log("page_mode - " + page_mode); log("dirty - " + dirty); if(dirty){ this.saveData(); } return done; }, //States:["Include", "Inactive", "Exclude"], createLink:function(type, obj){ var infos = []; switch(typeof(type)) { case "number": case "string": infos = this.linkTypes[type] || []; if(!isArray(infos)) infos = [infos]; break; case "object": infos = Array.isArray(type)?type:[type]; break; default: return; } let prefix = this.local?"":"http://"+this.host+"/"; for(var i = 0; i < infos.length; i++){ let info = infos[i]; let a; if(info.url || info.href){ var filtered = objFilter(obj, info.fields, undefined, info.escape, true); if(filtered){ a = document.createElement("a"); if(info.url){ let o = objMerge(filtered, info.pairs); if(info.serialize || infos.serialize) o = objMap(o, true, undefined, info.serialize || infos.serialize); a.href = prefix + info.url+"?"+obj2post(o); } else if(info.href){ let href = info.href; let serialize = info.serialize || NoOp; for (let [name, value] in new Iterator(objMerge(filtered, info.pairs))) href = href.replace("{"+name+"}", serialize(value)); a.href = prefix + href; } } } else if(info.post){ var filtered = objFilter(obj, info.fields, undefined, info.escape, true); if(filtered){ a = document.createElement("a"); a.classList.add("javascript-submit-link"); a.classList.add("jsl-"+type); let href = info.post; if(info.pairs){ let serialize = info.serialize || NoOp; for (let [name, value] in new Iterator(objMerge(filtered, info.pairs))) href = href.replace("{"+name+"}", serialize(value)); } let form = document.createElement("form"); form.method="POST"; form.action = prefix + href; let serialize = info.serialize || NoOp; if(info.extras) filtered = objMerge(filtered, info.extras); if(info.optional) filtered = objFilter(obj, info.optional, filtered); for(let name in filtered) { let input = document.createElement("input"); input.type="hidden"; input.name=name; input.value = serialize(filtered[name]); form.appendChild(input); } a.appendChild(form); let name; do name = "jslf" + Math.floor(Math.random()*10000); while(window.document.forms.namedItem(name)); form.id = form.name = name; a.href = "javascript:document.forms."+name+".submit();"; a.setAttribute("form", name); } } if(a) { if(arguments.length > 2) { for each(var item in Array.slice(arguments, 2)) if(typeof(item) === "string") a.appendChild(document.createTextNode(info[item] || infos[item])); else a.appendChild(item); } let title = info.title || infos.title; if(title) a.title = title; a.setAttribute("createLink", type); return a; } } return; }, /**/ resizeLinkCheck:function(root){ let that = this; let a = compileXPath("./a"); let submit = compileXPath("./form/input[@type='submit']"); $Z(compileXPath(".//div[contains(concat(' ',@class,' '), ' reformed-link ') and .//input[contains(concat(' ',@class,' '), ' fix-me ')]]"), function(div){ let s = $X(submit, div); s.classList.remove("fix-me"); that.convertLinkSize($X(a, div), s); }, root); }, convertLinkSize:function(a, submit){ let w = a.offsetWidth; let h = a.offsetHeight; if(w == 0 && h == 0) submit.classList.add("fix-me"); else { submit.style.width=w+"px"; submit.style.height=h+"px"; } //log(Array.concat(arguments, w, h)); }, convertLink:function(a){ let name = a.getAttribute("form"); if(name){ let div = document.createElement("div"); div.className=a.parentNode.className; div.classList.add("reformed-link"); let form = $X("//form[@name='"+name+"']"); form.className="reformed-link"; a.classList.add("reformed-link"); div.appendChild(form); insertAsParent(div,a); //let cs = window.getComputedStyle(a); let submit = document.createElement("input"); submit.type="submit" //submit.value = a.textContent.trim(); submit.classList.add("fix-me");//hack!!! Sizes are wrong the first time around. This is probably due to the point in time when this gets injected. this.convertLinkSize(a,submit); form.appendChild(submit); if(a.title) submit.title = a.title; // form.setAttribute("title", a.getAttribute("href")); a.removeAttribute("href"); a.href = "javascript:void(null);" } },/**/ update_page_map:{ // max:"Help-Policies-Borrowing", // formats:"Help", // features:"Help-Policies-Account", // lending:"help-lendingpolicies", // waitinglist:"MyWaitingList", // checkedout:"MyBookshelf", // lending: new UpdateURL("LendingPolicies"), // account: new UpdateURL("MyAccount", {TitleLimit:0, PerPage:100}), }, }); Provider.ProviderData = { //This is here so that it can access the provider data without having to have it passed in or re-evaluate it. //The data is used to nuke the waitinglist and checkout links. - Styling has changed so it doesn't work... most likely ignorable:function(obj){ let data = this; let m = function(i){return obj.id == i.id && obj.format == i.format; }; obj.getAdditional({ BrokenScript:{xpath:".//script[contains(text(), 'document.write')]/following-sibling::*[1][self::noscript][not(@broken)]", parse:function(noscript){//The script is broken, so give me the noscript (to much work to fix script). let found = evalNoScript(noscript.textContent); if(found.some(function(r){return r.nodeName != "#text"})) { noscript.setAttribute("broken", true); let last = noscript.nextSibling; for each(var node in found) noscript.parentNode.insertBefore(node, last); } else noscript.setAttribute("broken", "unknown"); //log(noscript, found, noscript.getAttribute("broken")) }}, /*Get:{xpath:[ ".//a[(starts-with(@href, 'BANGCart.dll?') and contains(concat(translate(@href, '?', '&'), '&'), '&Action=Add&')) or (starts-with(@href, 'BANGAuthenticate.dll?') and contains(concat(translate(@href, '?', '&'), '&'), '&Action=AuthCheck&') and contains(translate(@href, '?', '&'), '&URL=WaitingListForm.htm%3F'))]", ].join(" | "), parse:function(a, i, obj, name, alt, results) { if(!obj.isNodeParsed(a, name)) { var link = search2obj(a.href.split("?")[1], true); if(link.action == "AuthCheck") link = search2obj(link.url.split("?")[1], true) if(link.id == obj.id && link.format == obj.format) { //results.push(a); obj.setNodeParsed(a, name); if(!"availability" in obj) obj.availability = (link.action == "Add")?"Add":"Hold"; if(!"onWaitingList" in obj) obj.onWaitingList = data.waitinglist.some(m); if(!"checkedOut" in obj) obj.checkedOut = data.checkedout.some(m); a.classList.add("checked"); if(obj.onWaitingList) { if(obj.availability != "Cart")//don't hold it if it is already on hold, but don't get in the way of placing it in the cart if we already have it on hold. { a.classList.add("strike"); a.title = "You already have this title on hold!" } } if(obj.checkedOut) { // if(link.action != "Add") { a.classList.add("strike"); a.title = "You have already have this title checked out." } } } } },},/**/ }); }, } //} var Providers = lazyProperties({ addAliases:function(childParent){ this.cachedAliases(); let dirty = false; for(let [child, parent] in Iterator(childParent)){ let kac = this.known_aliases[child]; let ip = this.known_aliases[parent] || parent; if(this.known_aliases[child] != ip) { this.known_aliases[child] = ip; dirty = true; } } if(dirty) this.saveAliases(); return this.known_aliases; }, addAlias:function(parent, child){ this.cachedAliases(); let kac = this.known_aliases[child]; let ip = this.known_aliases[parent] || parent; if(kac != ip) { log(child, ip); this.known_aliases[child] = kac = ip; this.saveAliases(); } return this.known_aliases[child]; }, // letter:objMerge(new Provider("us.newsletter.overdrive.com", "Include", Parser.provider.Letter), {noSearch:true, noAutoUpdate:true, stealth:true}), saveAliases:function(){ this.Supported;//we need this cached up to this point if(!this.known_aliases) return this.known_aliases = this.getAliases(); let raw = this.aliasesToRaw(this.known_aliases); if(raw !== undefined) GM_setValue("aliases", JSON.stringify(raw)); else GM_deleteValue("aliases"); this.known_aliases = this.rawToAliases(raw || {}); //active providers may need to be moved. let ap = this.activeProviders; let dirty = false; for(let [i, ph] of Iterator(ap)){ let k = this.known_aliases[ph]; if(k && ph != k){ let p = this.Supported[k] = this.Supported[ph]; delete this.Supported[ph]; if(p instanceof Provider) p.updateHost(k);//this moves the data associated with the host. ap[i] = k; dirty = true; } } if(dirty) this.activeProviders = ap; this.inactiveProviders = this.resolveProviders(this.inactiveProviders); return this.known_aliases; }, aliasesToRaw:function(aliases){ for (let i in aliases){ let check = /^[^.]+\.libraryreserve\.com$/; let out = aliases; let individuals = {}; let groups = new Set(); let raw = {}; log(individuals, groups, raw); for(let child in out){ let parent = out[child]; if(child != parent){ let gc = individuals[child]; let gp = individuals[parent]; if(gc){ if(gp){ //if(gc.length > gp.length)//we do this so we don't have to iterate over the larger set // [gc, gp, /*gc.parent, gp.parent*/] = [gp, gc, /*gp.parent, gc.parent*/];//should swap the parents but it don't really matter groups.remove(gc); for(let value of gc){ gp.push(value); individuals[value] = gp; } let t = gp.parent || gp.parent; if(t) quickSetValueProperty(gp, "parent", t, false); } else { gc.unshift(parent); gp = individuals[parent] = gc; if(!gp.parent && check.exec(parent)) quickSetValueProperty(gp, "parent", parent, false); } } else if(gp){ gp.push(child); individuals[child] = gp; if(!gp.parent && check.exec(child)) quickSetValueProperty(gp, "parent", child, false); } else { groups.add(gp = individuals[child] = individuals[parent] = [parent, child]); if(check.exec(child)) quickSetValueProperty(gp, "parent", child, false); else if(check.exec(parent)) quickSetValueProperty(gp, "parent", parent, false); } } } groups.forEach(function(group){ if(group.parent) raw[group.parent] = group.filter((v) => v != group.parent); else raw[group[0]] = group.slice(1); }); return raw; } return undefined; }, rawToAliases:function(raw){ let out = {}; for(let name in raw){ if(name != "undefined"){ out[name] = name;//makes life potentially easier. for(let item of raw[name]) if(item != "undefined") out[item] = name; } } return out; }, getAliases:function(){ let t = GM_getValue("aliases", ""); if(t == ""){ return { "emedia2go.org":"dcl.lib.overdrive.com", "listennjnw.com":"listennjnw.lib.overdrive.com", "elibrarynj.com": "www.listennj.com", "listennj.com": "www.listennj.com", "infolink.libraryreserve.com": "www.listennj.com", "listennj.lib.overdrive.com": "www.listennj.com", "poudre.lib.overdrive.com": "overdrive.poudrelibraries.org", "go.live-brary.com": "suffolkwave.org", "www.suffolkwave.org": "suffolkwave.org", }; } return this.rawToAliases(JSON.parse(t)); }, getAliasesFor:function(host){ return aliasesToRaw(cachedAliases())[host] || []; }, cachedAliases:function(){ return this.known_aliases || (this.known_aliases = this.getAliases()); }, get activeProviders(){ var t = GM_getValue("domains", ""); return t?t.split("/").filter(function(f){if(f != "librarybin.com") return f;}):[]; }, set activeProviders(v){ if(v == null || v.length == 0) GM_deleteValue("domains"); else GM_setValue("domains", v.filter(function(f){if(f != "librarybin.com") return f;}).join("/")); }, get inactiveProviders(){ var t = GM_getValue("domains-inactive", ""); return t?t.split("/").filter(function(f){if(f != "librarybin.com") return f;}):[]; }, set inactiveProviders(v){ if(v == null || v.length == 0) GM_deleteValue("domains-inactive"); else GM_setValue("domains-inactive", v.filter(function(f){if(f != "librarybin.com") return f;}).join("/")); }, storeProvider:function(host, mode, obj){ var provider = this.getProvider(host); var Host = provider.host; provider.mode = mode; if(arguments.length > 2) provider.data = obj; var filter = function(f){return f != Host;}; this.activeProviders = (mode === "Include")?this.activeProviders.filter(filter).concat(Host):this.activeProviders.filter(filter); this.inactiveProviders = (mode === "Inactive")?this.inactiveProviders.filter(filter).concat(Host):this.inactiveProviders.filter(filter); return this.Supported[Host] = provider; }, getProvider:function(host, local){ var Host = this.dealiasProvider(host, local) var value = this.Supported[Host]; switch(typeof(value)){ default: if(value != null) return value; case "undefined": value = "Exclude"; case "string": return this.Supported[Host] = new Provider(Host, value, local); case "boolean": return this.Supported[Host] = new Provider(Host, value?"Include":"Inactive", local); } }, dealiasProvider:function(host, local){ //check known aliases //we need to resolve aliases in a clever this.cachedAliases(); if(local) { /* SourceHost is HTTPOnly AHHHHHHHH let c = Cookies.getCookies(); let sh = c[("SourceHost")] || []; if(sh.length){ //this.saveAliases();//used to flatten everything first //assume it's flat let dirty = false; for(let ho of sh){ let old = this.known_aliases[host]; if(old != ho) { this.known_aliases[host]= ho; dirty = true; } } if(dirty) log(this.saveAliases());//used to save and flatten }*/ } return this.known_aliases[host] || host; }, resolveProviders:function(hosts){ //check known aliases //highlighting for search.overdrive.com doesn't work properly if the servers don't match this.cachedAliases(); return hosts.map((host) => this.known_aliases[host] || host); }, }, { Supported: function(){ var me = {}; for(var n of this.activeProviders) me[n]="Include"; for(var n of this.inactiveProviders) me[n]="Inactive"; // me[Providers.letter.host] = Providers.letter; return me; }, }); //{ CookieMonster function CookieMonster(doc){ this.doc = doc || document; } CookieMonster.prototype = installSelfDestructingGettersAndSetters({ refresh:function(){ delete this.cookies; }, getCookie:function(name){ let list = this.cookies[name]; if(!list){ this.cookies[name] = list = []; let values = (";"+this.cache).split(";"+name+"="); for(let i = 1; i < values.length; i += 2) list.push(values[i].split(";")[0]); } return list; }, getCookies:function(){ this.cookies = {}; for(let pair of this.doc.cookie.split(";")){ let [,name, value] = /([^=]*)=(.*)/.exec(pair); let list = this.cookies[name]; if(!list) this.cookies[name] = list = []; list.push(value); } return this.cookies; } }, { doc: function() document, cookies: function(that) that.getCookies(), cache: function(that) that.doc.cookies, }, true); //} lazyProperty(this, "Cookies", function(){ return new CookieMonster(document); }); lazyProperty(this, "Formats", function(){ //{ Format function Format(title, type, software, alts){ this.title = title; this.type = type; this.software = software; this.alts = alts || []; } Format.prototype = { get names(){ return [this.title].concat(this.alts || []); }, }; function FormatClass(title, titles){ this.titles = titles || title; this.title = title; this.formats = []; } //} var names = {}; var softwares = {}; var classes = { "audiobook":new FormatClass("Audiobook", "Audiobooks"), "ebook":new FormatClass("eBook", "eBooks"), "video":new FormatClass("Video", "Videos"), "music":new FormatClass("Music"), //"online":new FormatClass("Online"), //"pending":new FormatClass("Pending"), "unknown":new FormatClass("Unknown"), }; var numbers = {//The best place to get numbers from is the Advanced Search page /*"PDF eBook" sometimes means 50 or 450*/ 1: new Format("Microsoft eBook", "ebook", "Microsoft", []),//Old and dead 25: new Format("OverDrive WMA Audiobook", "audiobook", "OverDrive", ["WMA Digital Audiobook"]), 30: new Format("OverDrive Music", "music", "OverDrive", []), 35: new Format("OverDrive Video", "video", "OverDrive", []), 40: new Format("OverDrive Video (mobile)", "video", "OverDrive", []),//Child Format 50: new Format("Adobe PDF eBook", "ebook", "Adobe", [/*Spanish "eBook de Adobe"*/]), 302: new Format("Disney Online Book", "ebook", "Online", ["Disney Digital Books Online", "Disney Online Books"]), 410: new Format("Adobe EPUB eBook", "ebook", "Adobe", ["Adobe EPUB eBook"]), 420: new Format("Kindle eBook", "ebook", "Amazon", ["Kindle Book", "Kindle Book (BETA)"]), 425: new Format("OverDrive MP3 Audiobook", "audiobook", "OverDrive", ["MP3 Digital Audiobook"]), 450: new Format("Open PDF eBook", "ebook", "Open", ["Open PDF"]), 610: new Format("OverDrive READ", "ebook", "Online", []), 635: new Format("Streaming Video", "video", "Online", []), 810: new Format("Open EPUB eBook", "ebook", "Open", ["EPUB eBook", "Open EPUB"]), 900: new Format("Mobipocket eBook", "ebook", "Mobipocket", []),//Old and dead // "-1": new Format("Pending", "pending", "", []),//Marketplace // "-100": new Format("Pending (eBook)", "pending", "", []),//Marketplace // "-200": new Format("Pending (Audiobook)", "pending", "", []),//Marketplace } let add = function(Num, format){ for(var name of format.names) names[name] = Num; classes[format.type].formats.push(Num); let n = softwares[format.software]; if(n) n.push(Num); else softwares[format.software] = [Num]; return format; } for (var [Num, format] in new Iterator(numbers)) { add(Num, format); } let obj = { Numbers:numbers, Names:names, Software:softwares, Classes:classes, NameLookup: function(name){ return obj.Names[name] || 0;}, NumberLookup: function(number){ return obj.Numbers[number] || add(number, obj.Numbers[number] = new Format("Unknown-" + number, "unknown", "Unknown"));}, SoftwareLookup: function(software){ return obj.Software[software] || [];}, } return obj; }); lazyProperty(this, "OverDrive", function(){ //{ Service function ServiceLink(info){ let target = info; let serialize = info.serialize || NoOp; if(info.post) { target.overwriteLink = function(a, obj){ if(a) { if(info.fields) obj = objFilter(obj, info.fields, undefined, info.escape, true); if(obj) { a.classList.add("reformed-link"); let form = document.createElement("form");//{ form.method="POST"; form.action = info.post form.className="reformed-link" //} for(name in obj) { let input = document.createElement("input"); input.type="hidden"; input.name=name; input.value = serialize(obj[name]); form.appendChild(input); } let div = document.createElement("div");//{ div.className=a.parentNode.className; div.classList.add("reformed-link"); div.appendChild(form); //} insertAsParent(div,a); let submit = document.createElement("input");//{ submit.type="submit" //submit.value = a.textContent.trim(); submit.style.width=a.offsetWidth+"px"; submit.style.height=a.offsetHeight+"px"; form.appendChild(submit); //} form.setAttribute("title", a.getAttribute(a.title?"title":"href")); a.removeAttribute("href"); return div; } } return; } } else if(info.javascript) { target.overwriteLink = function(a, obj){ if(!a){ a = document.createElement("a"); a.setAttribute("generated", true); } let escape = info.escape || NoOp; let args = info.arguments.map(function (name, to){return serialize(escape(GetFieldValue(obj, name, to), obj, name, to));}); if(args.indexOf(undefined) != -1) return;//X_X a.href = "javascript:"+info.javascript + "(\"" + args.join("\", \"") + "\")"; return a; } } else { target.overwriteLink = function(a, obj){ var filtered = info.copyFields?obj:objFilter(obj, info.fields, undefined, info.escape, true);//This is correct. if(filtered){ if(!a){ a = document.createElement("a"); a.setAttribute("generated", true); } if(info.url){ let o = objMerge(filtered, info.pairs); if(info.serialize) o = objMap(o, true, undefined, info.serialize); a.href = info.url + "?" + obj2post(o); } else if(info.href) { let href = info.href; for (let [name, value] in new Iterator(objMerge(filtered, info.pairs))) href = href.replace("{"+name+"}", serialize(value)); a.href = href; } else return; return a; } } } return target; } function Service(name, title, abbr, hosts, links, parse){ this.service = name; this.title = title; for(let host of hosts) od.Hosts[host] = this; for(let name in links) { let link = links[name]; if(!Array.isArray(link)) links[name] = link = [link]; for(let x = 0; x < link.length; x++) ServiceLink(link[x]);//this installs the correct overwriteLink on each of the links (these are called by Service.overwriteLink). } this.links = links; this.abbreviation = abbr; if(typeof parse === "function") this.parse = parse; od.Services[name] = this; ProviderBase.call(this); } Service.prototype = objCreate(ProviderBase.prototype, { createLink: function(type, obj){ var a = this.overwriteLink.apply(this, [null].concat(Array.slice(arguments))); if(a){ a.setAttribute("createLink", type); a.setAttribute("service", this.service); } return a; }, overwriteLink: function(a, type, obj){ let ret = null; let inf = this.links[type]; if(inf) { let service = this; let info; for(let x = 0; (x < inf.length) && (ret == null); x++){ info = inf[x]; ret = info.overwriteLink(a, obj); } if(ret) { if(!a)//find it if it has been generated by us. a = $X(compileXPath("descendant-or-self::a[@generated]")); let title = info.title || inf.title || service.title; if(a) { title = title || a.title; a.title = title; a.setAttribute("service", service.service); } if(title !== a)//I think this is a bug //FIXME ??? ret.title = title; for (let i = 3; i < arguments.length; i++) { ret.appendChild(arguments[i]); } } } return ret; }, createOtherServiceLinks: function(node, obj, type, showAll){ // log(arguments) let last = node.appendChild(document.createTextNode("[")); type = type || "title"; let show = globalSettings.serviceLinks; // log(show, showAll) for each(let info in OverDrive.Services){ // let info = this.Services[n]; if(info !== this && info.links[type] && (showAll || show[info.service])){ let a = info.createLink(type, obj, document.createTextNode(info.abbreviation)); // log(info, node, type, a) if(a) { node.appendChild(a); node.appendChild(last = document.createTextNode(", ")); } // else log(info, type, obj, n); } } if(last.nodeValue == ", ") last.nodeValue = "] ";//need a text node there, no point in not using it. else last.nodeValue = " ";//no services so no brackets. return node; }, parse: function(bit){return;}, }); //} let od = { Services: {}, Hosts: {}, getService: function(domain, path) { return od.Hosts[domain]; }, createServiceLinks: Service.prototype.createOtherServiceLinks, createGlobalWishlistCheckbox:function(node, obj){ var tobj = objFilter(obj, {"id":["id", 0],"format":["format", 1]}); node.appendChild(objMerge(textcheckbox(" Wishlist", globalWishlist.obj2item(tobj), function(e){ if(e.currentTarget.checked) globalWishlist.add(tobj); else globalWishlist.remove(tobj); }, globalWishlist.has(tobj)), {className:"nowrap"})); node.appendChild(document.createTextNode(" ")); return node; }, }; let overdrive_base = "http://www.overdrive.com/" //let overdrive_classic = overdrive_base + "classic/" /* Search uses AQS (Advanced Query Syntax) http://msdn.microsoft.com/en-us/library/aa965711%28v=vs.85%29.aspx values are not case sensative but keywords and operators are. boolean AND/OR must be upper case modern search supports isbn searching, just put in the ISBN number and it opens up the TitleInfo page. you can also search for the UUID as well, AQS does not do floating point numbers Note the WorldCat link: http://worldcatlibraries.org/wcpa/oclc/62300324 https://web.archive.org/web/20070305034212/http://search.overdrive.com/TitleInfo.asp?ID={90071AE2-FA3A-4A9F-AA5F-AE5B2F9739C1}&Format=25 http://www.contentreserve.com/TitleInfo.asp?ID=90071ae2-fa3a-4a9f-aa5f-ae5b2f9739c1&Format=25 http://western.opac.marmot.org/EcontentRecord/2935666/Home Look at the marc record: it's 001 is ovr62300324 You can search by the world cat number 62300324. I've tried to guess the field name with no success Unfortunately there doesn't seem to be an easy way to determine the worldcat number. http://catalog.library.nashville.org/EcontentRecord/40782 Google: "PublisherCatalogNumber" overdrive -"marmot.org" edition:unabridged publisher:Penguin imprint:Penguin title:Test series:test subtitle:test - similar to series, which is often a componant but not always contentCreatorNames: subjects.name:Fantasy formatClassification:eBook, Audiobook or Video formats.formatName:"Kindle Book" formats.formatID:25 regionalAvailability:"United States" region:"New York" fullDescription: shortDescription: popularity - integer, seems to max out around 140000, I don't know how this is calculated. publishDate languages:en - you can sort by "language" fileSize - integer, unit is byte but it as part of the query it only has to match one of the fileSize attributes of the various formats. So fileSize:(<1 AND >1) will return values! ageGroup - movie ratings! duration - only exact time values work, < and > don't work. numbers don't seem to work about as useful as fileSize fileName:HeroesoftheValley-3135 <-- this is the odm filename http://fulfill.contentreserve.com/HeroesoftheValley-3135.odm? (plus a bunch of params) partCount:0 source:"From the book" - refers to samples There is a field for content creator by "last, first" I don't know it's name These three are equivilent contentCreatorNames:"J.R.R. Tolkien" OR contentCreatorNames:"J. R. R. Tolkien" contentCreatorNames:("J.R.R. Tolkien" OR "J. R. R. Tolkien") contentCreatorNames:("J.R.R. Tolkien" "J. R. R. Tolkien") Note: To search for content released on a specific date, use the following: "publishDate:<=2013-03-20 AND publishDate:>=2013-03-20" and not: "publishDate:2013-03-20" publishDate:<=2013-03-20 == publishDate:<2013-03-21 formatName:"Kindle Book" resolves to something like formats.formatName:"Kindle Book" formatID: resolves to something like supplemental.formatID but I'm not sure what the parent class is or something I think the prefix is something that starts with a value less than 'f' since formatID does not resolve to formats.formatID. I've tried the following with/without 'formats'. *enhancements *enhancement *enhances *enhanced *enhance *supplemental *supplement content.enhance content.enhanced contentEnhance contentEnhanced enhancedDetails supported operators are: < <= > >= - AND OR NOT https://search.overdrive.com/autocomplete/titles/?query=fullDescription%3Awolf%20AND%20publishDate%3A%3E2013-2-01 */ //the core needs to be rewritten so that that encodeURIComponent happens as the URL is constructed. It should not happen on the individual componants like we are doing below. function smart_serial(value){ return encodeURIComponent(emQuote(value)); } let f = "useAutoFilters=f" let af = "&"+f; let qf = "?"+f; new Service("modern", "OverDrive Modern", "OD", ["www.overdrive.com", "search.overdrive.com"],{ imprint:{href:overdrive_base+"search?q=imprint%3A{imprint}"+af, fields:["imprint"], serialize: smart_serial}, title:{href:overdrive_base+"ti/{id}", fields:["id"]}, // title:{href:overdrive_base+"TitleInfo/{id}/{format}", fields:["id", "format"]}, // title:{href:overdrive_base+"TitleInfo.aspx?ReserveID={id}&FormatID={format}", fields:["id", "format"]}, search:{url:overdrive_base+"SearchResults.aspx", copyFields:true},//probably doesn't work "title-search":[//{ Note: we trim off leading "The" if it exists, this is not strictly necessary as OverDrive does it automagically {href:overdrive_base+"search?q=title%3A{title} AND contentCreatorNames%3A{creator}"+af, fields:["title","creator"], serialize: smart_serial, escape:processForSort}, {href:overdrive_base+"search?q=title%3A{title}"+af, fields:["title"], serialize: smart_serial, escape:processForSort}, ],//} // "title-search":{href:overdrive_base+"ti/{id}", fields:["id"]},//this is not a solution // "title-search":{href:overdrive_base+"SearchResults.aspx?ReserveID={id}", fields:["id"], title:"OverDrive Title Search"}, series:objMerge([//{ {href:overdrive_base+"search?q=series%3A{Series}&f-contentCreatorNames={creator}"+af, fields:{"creator":((t) => (t.creator && encodeURIComponent(t.creator))), "Series":((t) => (t.Series && smart_serial(t.Series)))}}, {href:overdrive_base+"search?q=series%3A{Series}"+af, fields:["Series"], serialize: smart_serial}, ], {title:"Search for other titles in this series in the OverDrive database",}),//} creator:objMerge([//{ {href:overdrive_base+"creators/{creatorNumber}/"+qf, fields:["creatorNumber"]}, {href:overdrive_base+"search?q=contentCreatorNames%3A{creator}"+af, fields:["creator"], serialize: smart_serial}, ], {title:"Search for other titles by this creator in the OverDrive database",}),//} publisher:objMerge([//{ // {href:overdrive_base+"publisher/{publisherNumber}/"+qf, fields:["publisherNumber"]},//doesn't work {href:overdrive_base+"search?q=publisher%3A{publisher}"+af, fields:["publisher"], serialize: smart_serial}, ], {title:"Search for other titles by this publisher in the OverDrive database",}),//} }, function(pathname){ //overdrive gutted and redesigned everything, none of this code works. let search = search2obj(location.search); let form = $X("//form[@action='/search']"); if(form){ window.setTimeout("htmlEncode = function(n){ return n; }", 0);//htmlEncode breaks everything and this is the easiest solution for fixing it. let afv = search.useAutoFilters || "f";//because this is really really annoying! if(form.useAutoFilters) form.useAutoFilters.value = afv; else form.appendChild(nodeMerge("input", {type:"hidden", name:"useAutoFilters", value:afv})); } let obj; let about = {}; let sp = this; let pathsplit = pathname.split("/"); let bit = pathsplit[1].toLowerCase(); log(bit); {//Media Types for Advanced Search. /* let targetXPath = null; $Z("//form[@action='/SearchResults/ShowResults']", function(form){ let target = $X(targetXPath = (targetXPath || compileXPath(".//div["+xPathListContains("@class", "left")+" and "+xPathListContains("@class", "advSearch")+"]")), form); if(target){ let box = document.createElement("div"); box.classList.add("advContainer"); let label = box.appendChild(document.createElement("div")); label.appendChild(document.createTextNode("Results Per Page: ")); label.classList.add("advLabel"); let controls = box.appendChild(document.createElement("div")); controls.classList.add("advControl");//left let options = controls.appendChild(document.createElement("select")); options.name = "ddlResultsPerPage"; for(let value of [10, 25, 50]) options.appendChild(document.createElement("option")).appendChild(document.createTextNode(value)); target.appendChild(document.createElement("div")).className="clear"; target.appendChild(box); target.appendChild(document.createElement("div")).className="clear"; } let formats = $X(".//select[@name='ddlMediaFormat']", form); if(formats){ let controls = insertBefore(document.createElement("div"), formats); let map = {}; function createGroup(text, modern){ let opt = document.createElement("option"); opt.innerHTML = text; let field = modern?modern+"Visible":""; opt.setAttribute("for", field); opt.value = "" opt.className = "indent1"; return {title:opt, field:field}; } $Z(compileXPath(".//input[@type='hidden' and starts-with(@name, 'hidShow')]"), function(r){ let type = r.name.replace(/^hidShow(.*?)s?$/, "$1").toLowerCase(); let text = Formats.Classes[type].titles; let obj = map[type] = createGroup(text, r.name); obj.checkbox = textcheckbox(""+text+"", r.name+"Visible", function(e){r.value = {true:"True", false:"False"}[this.checked];}, {True:true, False:false}[r.value]); }, form); //map.online = createGroup("Online", opt.getAttribute(map.ebook));//Modern doesn't have our Online designation for(let name of Object.keys(map).sort()){ let item = map[name]; formats.appendChild(item.title); if(item.checkbox) controls.appendChild(item.checkbox); } map.online = map.ebook;//Modern doesn't have our Online categorization. map.unknown = createGroup("Unknown", "");//This lets the sorting deal with unknown types and not implode. for(let opt of Array.slice(formats.options)){ if(opt.value){ let type = map[ Formats.NumberLookup(opt.value).type]; let last = type.last || type.title; if(!last.parentNode) formats.appendChild(last); type.last = insertAfter(opt, last);//keeps the ordering opt.classList.add("indent2"); opt.setAttribute("for", type.field); } } function setCheckbox(node, value){ // log(node.checked != value, node, value, node.checked) if(node.checked != value) node.click(); } addEvent(formats, "change", function(e){ let target = this.options[this.selectedIndex].getAttribute("for"); if(target) $Z(compileXPath(".//input[@type='checkbox' and starts-with(@id, 'hidShow')]"),function(r){ setCheckbox(r, r.id == target);}, this.form); else $Z(compileXPath(".//input[@type='checkbox' and starts-with(@id, 'hidShow')]"), function(r){ setCheckbox(r, true); }, this.form); }); } });*/ GM_addStyle([//{ "#ddlMediaFormat .indent1 { margin-left:1em; }", "#ddlMediaFormat .indent2 { margin-left:2em; }", ".advLabel { width: 8.5em; }", ".advControl input[type='checkbox'] { width:auto; }", "ul.meta-accordion dl { cursor:default; }",//March 2014 - Looks silly to have a hand where you can't click. ].join("\n\n")).id="AdvancedSearchMangler"//} } switch(bit) { case "":{//homepage obj = []; $Z("//ul/div/li/article", function(r,i){ let title = new ServiceItem([r], null, null, { "Image":"./div["+xPathListContains("@class", "cover")+"]/a/img", "Title":"./div["+xPathListContains("@class", "details")+"]/header/h3["+xPathListContains("@class", "title")+"]", "Subtitle":{xpath:"following-sibling::h3["+xPathListContains("@class", "subtitle")+"]", node:"Title-node"}, //"Creator":{xpath:"following-sibling::h4["+xPathListContains("@class", "creator")+"]", node:"Subtitle-node"}, "Meta":"./div["+xPathListContains("@class", "details")+"]/div["+xPathListContains("@class", "meta")+"]", CreatorPairs:{xpath:"following-sibling::h4["+xPathListContains("@class", "creator")+"]", node:"Title-node", parse:function(node, i, obj, name, inst, results){ let creators = []; for(let n of node.childNodes){ if(n.nodeName == "#text"){ name = n.textContent.trim(); if(name){ let role = $X("following-sibling::span["+xPathListContains("@class", "role")+"]", n); let text = SplitTextNode(n, name); let creator = { name:name, id:undefined, number:undefined, link:insertAsParent(sp.createLink("creator", {creator:name}), text), text:text, role:(role && role.textContent.trim() || "Author"), }; creators.push(creator); } } } if(creators.length) obj.creatorObjs = creators; return node; /*let search = search2obj(node.search, true); let field; if("screator" in search) field = "Creators"; else if("spublisher" in search) field = "Publisher"; let res = obj.processAttribute(field, null, node.parentNode); if(res !== undefined) results.push([field, node.parentNode, res]); return res;*/ }, }, }); if(title.subtitle) { let node = title.nodes["Subtitle-value"]; let link = sp.createLink("series", {Series:processForSort(title.subtitle)}); link.appendChild(document.createTextNode("S")); let span = document.createElement("span"); span.className = "topup"; span.appendChild(document.createTextNode(" [")); span.appendChild(link); span.appendChild(document.createTextNode("] ")); node.appendChild(span); } obj.push(title); }); /* let form = $X("//form[@id='search']"); if(form){ let search = form.searchInput; let div = insertAfter(document.createElement("span"), form); let axis = div.appendChild(document.createElement("select")); let ds; { function dts(date){log(date); return date.getFullYear()+"-"+(date.getMonth() + 1)+"-"+date.getDate();} let now = Date.now(); let today = new Date(now); let tomorrow = new Date(now + 86400000); ds = "publishDate:>=" +dts(today)+ " AND " + "publishDate:<" +dts(tomorrow); }//less than tomorrow, but greater than or equal to today. let nodes = {"":"","Title":"title:", "Author":"contentCreatorNames:","Series":"series:", "Date":ds, "English":"languages:en", "Not Abridged":"NOT edition:abridged"}; for(let [name, value] in Iterator(nodes)){ let props = {}; if(typeof value === "string" && value) props.value = value; let n = axis.appendChild(nodeMerge("option", name, props)); } addEvent(axis, "change", function(e){ if(this.value){ if(search.value.trim()){ search.value += " AND "; } search.value += this.value; //search.selectedOptions } }); }//*/ break; } //case "ti": //case "titleinfo": //case "titleinfo.aspx": //case "titleinfo.asp": case "media":{ //does not handle drm block. var title, book; obj = new ServiceItem(["//article[@id='media']"], null, null, { "Title":".//header["+xPathListContains("@class", "pageHeader")+"]/h1["+xPathListContains("@class", "title")+"]", "Subtitle":{xpath:"following-sibling::h2["+xPathListContains("@class", "subtitle")+"]", node:"Title-node"}, ID:{xpath:".//@data-reserve-id", parse:function(node, i, obj, name, inst, results){if(!obj.id) obj.id = node.value; return node; }}, //"FormatIcon":".//img[@id='imgFormatIcon']", "ServicerTypes":".//div[@id='rating']", "CreatorRoot":"./header["+xPathListContains("@class", "pageHeader")+"]/h2["+xPathListContains("@class", "creator")+"]",//it's possible to parse this but easier to "Creators":{xpath:".//aside/ul["+xPathListContains("@class", "meta-accordion")+"]/li/span["+xPathListContains("@class", "label")+" and contains(text(),'Creators')]", parse:function(node, i, obj, name, inst, results){ let creators = []; $Z(compileXPath("./../dl/dt"), function(r,i){ let vn = $X(compileXPath("./following-sibling::dd[1]"), r); let value = vn.textContent.trim(); let a = $X(("a[contains(text(),"+SmartQuotes(value)+")]"), obj.nodes["CreatorRoot-node"]); let number = undefined; if(a){ number = a.pathname.split("/")[2]; vn = replace(a.cloneNode(true), vn.childNodes[0]); } else { //we may be able to pickup the creator number later if it's picked up in Groups. vn = a = insertAsParent(sp.createLink("creator", {creator:value}), vn.childNodes[0]); } creators.push({name:value, id:undefined, number:number, link:a, text:(a||vn).childNodes[0], role:r.textContent.replace(/^[\s]*(.*)[\s]+:[\s]*$/,"$1"),}); }, node); if(creators.length) obj.creatorObjs = creators; },}, "Groups":{xpath:".//aside/ul["+xPathListContains("@class", "meta-accordion")+"]/li/span["+xPathListContains("@class", "label")+"]", parse:function(node, i, obj, name, inst, results) { let title = node.textContent.trim(); let r = node.parentElement; let fn = Formats.NameLookup(title); if(fn){ if(Array.isArray(obj.formats)) obj.formats = {}; var f = new FormatItem(obj, [r]); f.formats.push(fn); f.getAdditional({ "Pairs":{field:"./dl/dt", value:"./following-sibling::dd[1]"}, }); obj.formats[fn] = f; if(obj.id){ let k = node.appendChild(document.createTextNode(" ")); var s = insertAfter(sp.createOtherServiceLinks(document.createElement("silly"), f, "title"), k); s.className="up smaller"; } } else { switch(title) { case "Creators": break;//already parsed case "Publication Details":{ obj.getAdditional({ "Pairs":{field:"./dl/dt", value:"./following-sibling::dd[1]"}, }, r); break; } default: let nr = /^About ([^()]*) \((.*)\)$/.exec(title); if(nr){ let [, name, role] = nr; let link = $X("./span["+xPathListContains("@class", "bio")+"]/span["+xPathListContains("@class", "authorLink")+"]/a", r); let number = (link.pathname.split("/")[2]); if(!Array.isArray(obj.creatorObjs)) obj.creatorObjs = []; let creator = obj.creatorObjs.find(function(co){return co.number == number || (co.number == undefined && co.name == name)}); if(creator) { creator.desc = $X("./span["+xPathListContains("@class", "bio")+"]/p", r).textContent.trim(); if(creator.number == null) { creator.number = number; creator.link.href = link.href;//our injected link has the wrong href, lets fix it. } } else//somehow you managed to get this far and find an otherwise undetected creator! I don't think this ever happens. obj.creatorObjs.push({name:name, id:undefined, number:number, link:link, text:undefined, role:role,}); } else log("Need clever parser for: "+title); } } },}, //"H3":{field:"h3/descendant-or-self::*[contains(text(), ':') and position()<3][not(*)]", value:function(r, i, obj, title, pair, results){return doWhile(r, "following-sibling::*[node()][1][not(self::h3)]")}}, /*"ctl00_ContentPlaceHolder1_lbl":{xpath:"//label[starts-with(@id,'ctl00_ContentPlaceHolder1_lbl')]", parse:function(node, i, obj, name, inst, results){ let field = node.id.substr(name.length); if(!field.endsWith("Label") && field != "Series") { let n = $X(compileXPath(".[not(node())]/..[count(label) = 1]"), node);//or @id = 'ctl00_ContentPlaceHolder1_lbl"+field+"Link' let res = obj.processAttribute(field, $X("//label[@id = 'ctl00_ContentPlaceHolder1_lbl"+field+"Label']") || (n && node), n || node); if(res !== undefined) results.push([field, n || node, res]); } },},*/ }); obj.number = Number(pathsplit[2]); // let id = sp.linkify(obj); //log(obj); // log(obj) //* var t = obj.nodes["Title-node"]; if(t){ title = $X("./text()", t); if(obj.id){ let k = t.appendChild(document.createTextNode(" ")); var s = insertAfter(OverDrive.createGlobalWishlistCheckbox(sp.createOtherServiceLinks(document.createElement("span"), obj, "title"), obj), k); s.className="up smaller"; } let jsl = []; let box = t.parentElement.appendChild(document.createElement("div")); for(let ph of Providers.activeProviders) { let p = Providers.getProvider(ph); let a = p.createLink("title", obj); if(a){ if(box.children.length) box.appendChild(document.createTextNode(", ")); a.appendChild(document.createTextNode(p.data.title || ph)); box.appendChild(a); if(a.classList.contains("javascript-submit-link")) jsl.push({p:p,a:a}); } } for(let link of jsl) link.p.convertLink(link.a); }//*/ //title && insertAsParent(sp.createLink("title-search", obj), title); obj && obj.provider_groups.forEach(function(p){ let title = obj.nodes[p+"-title"]; if(title.nodeName == "TD") title = title.lastChild; let style = GM_addStyle(""); if(title) insertAfter(textcheckbox("Show All", "toggle-"+p, function(e){ style.innerHTML = (this.checked?"":"span.subscription-none, span.subscription-inactive { display:none; }"); }, true), title); }); GM_addStyle([//{ ".formatText .formatInfoValue { width:auto; }", // "#titleInfo { width: auto; max-width:36.19%; }", "#titleInfo .title { width: auto; }", ".smaller.up { font-size:50%; }", ].join("\n\n")).id="TitleInfoMangler"//} break; } case "creators": {//falls through to search if(typeof about.type === "undefined"){ about.type = bit; about.autofiltering="f"; let title = $X("//header["+xPathListContains("@class", "pageHeader")+"]/h1"); $X("//input[@id='searchInput']").value = "contentCreatorNames:"+emQuote(title.textContent.trim()); about.creator = { title:title, name:title.textContent.trim(), number:Number(pathsplit[2]), }; let box = title.parentElement.appendChild(document.createElement("div")); let jsl = []; let tobj = {creatorNumber:about.creator.number, creator:about.creator.name}; for(let ph of Providers.activeProviders) { let p = Providers.getProvider(ph); let a = p.createLink("creator", tobj); if(a){ if(box.children.length) box.appendChild(document.createTextNode(", ")); a.appendChild(document.createTextNode(p.data.title || ph)); box.appendChild(a); if(a.classList.contains("javascript-submit-link")) jsl.push({p:p,a:a}); } } let a = sp.createLink("creator", {creator:about.creator.name}); if(a){ insertAsParent(a, title); if(a.classList.contains("javascript-submit-link")) jsl.push({p:p,a:a}); } for(let link of jsl)//this will never happen but it's best to have it just in case. link.p.convertLink(link.a); } } //case "searchresults.aspx": //case "searchresults": case "subjects":{//falls through to search if(typeof about.type === "undefined"){ about.type = bit; about.autofiltering="f"; let title = $X("//header["+xPathListContains("@class", "pageHeader")+"]/h1"); $X("//input[@id='searchInput']").value = "subjects.name:"+emQuote(title.textContent.trim()); } } case "search":{ obj = []; if(typeof about.type === "undefined"){//Fix stupid overdrive bug. about.type = bit; about.autofiltering="f"; let q = search.q.replace(/\+/g, " "); if(q.indexOf(">") != -1){//cleanup time! This is why they did HTML escapes on input. They should have done HTML escapes when they wrote the value. This is a security risk. let s = $X("//span["+xPathListContains("@class", "queryContainer")+"]"); {//Checks to see if the security risk has been exploited. let body = new DOMParser().parseFromString("", "text/html").body; body.innerHTML=q; let n = $X(".//*", body); if(n){ window.location = "data:text/html;base64,PGh0bWw+PGJvZHk+PGgyPlNlY3VyaXR5IEV4Y2VwdGlvbiE8L2gyPklmIHlvdSBhcmUgc2VlaW5nIHRoaXMgaXQncyBiZWNhdXNlIHlvdSB3ZXJlIGRpcmVjdGVkIHRvIGEgVVJMIHRoYXQgaXMgdHJ5aW5nIHRvIGF0dGFjayB5b3UuPC9ib2R5PjwvaHRtbD4="; throw { message : "HTML Content Injection Detected! This is very very bad.", name : "Security Warning", }; } } let k = $X("./*[descendant-or-self::input[@id='searchInput']]",s); while(k.nextSibling) remove(k.nextSibling); //while(k.previousSibling) // remove(k.previousSibling); } if(q) $X("//input[@id='searchInput']").value = q; /* let t = $X("//span[@id='field-validation-summary']") let observer = new MutationObserver(function(mutations) { if(t.textContent.trim() == "You already have a saved search with this name"){ //to get this to work, rebuild "search" but with a name. Then the update will take. The easiest way to attack the "OverDrive" object would be through a proxy infection. That way I could get at what it's children contain. You could use bindDelete, fake the click? and then call bindSave again. } }); observer.observe(t, { childList: true, characterData: true }); */ } $Z("//a[@href]", function(a){ if(a.pathname == location.pathname){ let s = search2obj(a.search); if(!s.useAutoFilters){ s.useAutoFilters = (s.useAutoFilters || search.useAutoFilters || about.autofiltering || "f"); a.search = "?"+obj2post(s); } } //log(location.pathname, a.pathname, a.search); }) $Z("//ul["+xPathListContains("@class", "mediaResults")+"]//li", function(r,i){ let title = new ServiceItem(r, r, null, { Title:".//h3["+xPathListContains("@class", "title")+"]/a", Image: ".//div["+xPathListContains("@class", "cover")+"]/a/img", //Sample: ".//div["+xPathListContains("@class", "listenBtn")+"]//a[img[@alt='Sample']]", FormatType: ".//a["+xPathListContains("@class", "typeIndicator")+"]", //Description:".//div["+xPathListContains("@class", "description")+"]", Subtitle:".//h3["+xPathListContains("@class", "subtitle")+"]", //Series:".//div["+xPathListContains("@class", "listItemSubtitle")+"]/a", //ServicerTypes:".//div["+xPathListContains("@class", "listItemFind")+"]", //Pairs:{field: ".//div["+xPathListContains("@class", "creators")+" and not(a)]/span", value:"./following-sibling::text()",}, Publisher:".//div["+xPathListContains("@class", "meta")+"]", CreatorPairs:{xpath: ".//h4["+xPathListContains("@class", "creator")+"]", parse:function(node, i, obj, name, inst, results){ let creators = []; for(let n of node.childNodes){ if(n.nodeName == "#text"){ name = n.textContent.trim(); if(name){ let role = $X("following-sibling::span["+xPathListContains("@class", "role")+"]", n); let text = SplitTextNode(n, name); let number = undefined; let creator = {name:name, id:undefined, number:undefined, link:undefined, text:text, role:(role && role.textContent.trim() || "Author"),}; let tobj = {creatorNumber:creator.number, creator:name}; if(about.creator && about.creator.name == name){ tobj.creatorNumber = creator.number = about.creator.number; } creator.link = insertAsParent(sp.createLink("creator", tobj), text); creators.push(creator); } } } if(creators.length) obj.creatorObjs = creators; return node; /*let search = search2obj(node.search, true); let field; if("screator" in search) field = "Creators"; else if("spublisher" in search) field = "Publisher"; let res = obj.processAttribute(field, null, node.parentNode); if(res !== undefined) results.push([field, node.parentNode, res]); return res;*/ },}, }); if(title.nodes["Title-node"]) title.number = Number(title.nodes["Title-node"].pathname.split("/")[2]); if(title.subtitle) { let node = title.nodes["Subtitle-value"]; let link = sp.createLink("series", {Series:processForSort(title.subtitle)}); link.appendChild(document.createTextNode("S")); let span = document.createElement("span"); span.className = "topup"; span.appendChild(document.createTextNode(" [")); span.appendChild(link); span.appendChild(document.createTextNode("] ")); node.appendChild(span); } obj.push(title); }); /* let countNodes = []; $Z("//span["+xPathListContains("@class", "morePaginationInfo")+"]", function(r){//I should rewrite this so it isn't such a crazy hack. There is almost no point in using regex. objFilter(/^[\s]*·[\s]*Showing[\s]+([1-9]\d*)[\s]*-[\s]*([1-9]\d*)[\s]+of[\s]+([1-9]\d*)/.exec(r.textContent), {"start":1, "end":2, "total":3}, countNodes); countNodes.push(SplitTextNode(findTextElementContaining(countNodes.end, r), countNodes.end)); }); if(!countNodes.length) countNodes = null; cell = insertAfter(document.createElement("div"), $X("//h5["+xPathListContains("@class", "sectionHeader")+"]")); obj = ListPage(cell, {},//{availability:true, filter:true, abridged:true, paging:true, countNodes: countNodes, provider:sp}, $X("//ul["+xPathListContains("@class", "mediaResults")+"]"), ".//li", function(row, j){ let title = new ServiceItem(row, $X(compileXPath("./preceding-sibling::tr[1]"), row) || row, null, { Title:".//div["+xPathListContains("@class", "title")+"]/a", Image: ".//div["+xPathListContains("@class", "listItemCover")+"]/a/img[@name='cover']", Sample: ".//div["+xPathListContains("@class", "listenBtn")+"]//a[img[@alt='Sample']]", FormatType: ".//div["+xPathListContains("@class", "formatType")+"]", Description:".//div["+xPathListContains("@class", "description")+"]", Subtitle:".//div["+xPathListContains("@class", "listItemSubtitle")+" and not(a)]", Series:".//div["+xPathListContains("@class", "listItemSubtitle")+"]/a", ServicerTypes:".//div["+xPathListContains("@class", "listItemFind")+"]", Pairs:{field: ".//div["+xPathListContains("@class", "creators")+" and not(a)]/span", value:"./following-sibling::text()",}, CreatorPairs:{xpath: ".//div["+xPathListContains("@class", "creators")+"]/a", parse:function(node, i, obj, name, inst, results){ let search = search2obj(node.search, true); let field; if("screator" in search) field = "Creators"; else if("spublisher" in search) field = "Publisher"; let res = obj.processAttribute(field, null, node.parentNode); if(res !== undefined) results.push([field, node.parentNode, res]); return res; },}, }); sp.linkify(title); // log(title); let t = title.nodes["Title-node"]; if(t){ t.appendChild(document.createTextNode(" ")); let s = insertAfter(OverDrive.createGlobalWishlistCheckbox(sp.createOtherServiceLinks(document.createElement("span"), title, undefined, true), title), t); s.className="up"; } return title; }, LinkAdjust, imageResizing);//log, // if(mode && obj.length && search2obj(location.search, true).reserveid) // document.title = document.title.replace(" -", ": \"" + obj[0].title + "\" by \"" + obj[0].creators[0] + "\" -"); */ break; } } return obj; }); new Service("reserve", "Content Reserve", "CR", [//{ "ebookreserve.com", "server1.glassbook.com",//wtf? "server2.glassbook.com",//wtf? "contentreserve.com", "www.contentreserve.com", "secure.contentreserve.com", ], {//} title:{url:"http://www.contentreserve.com/TitleInfo.asp", fields:{"ID":"id", "Format":"format"}}, enhanced:{url:"http://www.contentreserve.com/EnhancedDetails.asp", fields:{"ID":"id", "Format":"format"}}, }, function(pathname){ let sp = this; let pathsplit = pathname.split("/"); let bit = pathsplit[pathsplit.length - 1].toLowerCase(); log(bit); let obj; switch(bit) { case "enhanceddetails.asp":{ //work on me //add a link to break; } case "": case "login.asp":{ var objs = []; $Z("//a[starts-with(@href, 'TitleInfo.asp?')]/ancestor::tbody[1]", function(r,i){ var obj = new Item([r]); obj.getAdditional({ Title:"tr[contains(concat(' ', @class, ' '), ' feature-title ')]/td/a", Image:".//img", Pairs:{ field:"tr[contains(concat(' ', @class, ' '), ' feature-detail ')]/td[contains(concat(' ', @class, ' '), ' feature-label ')]", value:"following-sibling::td[1]"}, }); obj.enhance(); sp.linkify(obj); var s = insertAfter(sp.createOtherServiceLinks(document.createElement("span"), obj, "title", true), obj.nodes["Title-node"]); objs.push(obj); }); log(objs); break; } //case "ti": //case "titleinfo": //case "titleinfo.aspx": case "titleinfo.asp": { //does not handle drm block. var title, book; obj = new ServiceItem($X("//td[comment()[contains(., ' **************************************************************************** ')]]"), null, null, { "Title":".//tr[@class='title']/td", "Subtitle":{xpath:"ancestor::tr[1]/following-sibling::tr[1][contains(@class, 'subtitle')]/td[@colspan='2']", node:"Title-node"}, "Pairs":{field:".//tr[contains(@class, 'shelf-detail')]/td[contains(@class, 'shelf-label') or contains(@class, 'subtitle')]", value:"./ancestor::tr[1][count(td) = 2]/td[2] | ./ancestor::tr[1][count(td) = 1]/following-sibling::tr[1]/td[1][not(contains(@class, 'shelf-label') or contains(@class, 'subtitle'))]"}, }); let o = search2obj(location.search, true); if(o.id && !obj.id) obj.id = o.id; if(obj.format == null && o.format) obj.formats.push(Number(o.format)); sp.linkify(obj); obj.nodes["Title-node"] && OverDrive.createGlobalWishlistCheckbox(sp.createOtherServiceLinks(obj.nodes["Title-node"], obj, "title", true), obj); obj.supplements && obj.supplements.forEach( function(ob){ sp.createOtherServiceLinks(ob.nodes["Title-node"], ob, "title", true); }); // obj.kick(); //title && insertAsParent(sp.createLink("title-search", obj), title); obj && obj.provider_groups.forEach(function(p){ let title = obj.nodes[p+"-title"]; if(title.nodeName == "TD") title = title.lastChild; let style_all = GM_addStyle(""); let style_overdrive = GM_addStyle(""); if(title){ if(p == "Retailer(s)"){//this is wrong insertAfter(textcheckbox("Hide Overdrive", "toggle-"+p+"-overdrive", function(e){ style_overdrive.innerHTML = (this.checked?"span.subscription-overdrive { display:none; }":""); }, false), title); insertAfter(document.createTextNode(" "), title); } insertAfter(textcheckbox("Hide Others", "toggle-"+p, function(e){ style_all.innerHTML = (this.checked?"span.subscription-none, span.subscription-inactive { display:none; }":""); }, false), title); } }); break; } } return obj; }); //DEAD - new Service("letter", "DLR Newsletters", "NL", { title:{url:"http://us.newsletter.overdrive.com/ContentDetails.htm", fields:{"id":"id"}}, }); new Service("video", "OverDrive Streeming Video", "SV", ["video.overdrive.com","videosamples.overdrive.com"], { title:{href:"http://videosamples.overdrive.com/?crid={id}", fields:{"id":((t) => ((t.Formats && t.Formats.some((f) => f == 635))?t.id:undefined)),},}, }, function(pathname){ let sp = this; let pathsplit = pathname.split("/"); let bit = pathsplit[1].toLowerCase(); log(bit); let obj; switch(bit) { case "watch":{ //does not handle drm block. obj = new ServiceItem($X("//div[@id='wrapper']"), null, null, { "Title":".//div["+xPathListContains("@class", "title")+"]", "Image":".//img["+xPathListContains("@class", "thumbnail")+"]", //"Description":".//span["+xPathListContains("@class", "description")+"]", "Creators":".//span["+xPathListContains("@class", "description")+"]//ul["+xPathListContains("@class", "creators")+"]", //"Subtitle":{xpath:"ancestor::tr[1]/following-sibling::tr[1][contains(@class, 'subtitle')]/td[@colspan='2']", node:"Title-node"}, //"Pairs":{field:".//tr[contains(@class, 'shelf-detail')]/td[contains(@class, 'shelf-label') or contains(@class, 'subtitle')]", // value:"./ancestor::tr[1][count(td) = 2]/td[2] | ./ancestor::tr[1][count(td) = 1]/following-sibling::tr[1]/td[1][not(contains(@class, 'shelf-label') or contains(@class, 'subtitle'))]"}, }); obj.formats = [635];//If it's streaming a sample, it will stream the entire video... I hope. sp.linkify(obj); for(var co of obj.creatorObjs){ let span = insertAfter(sp.createOtherServiceLinks(document.createElement("span"), {creator:co.name}, "creator", true), co.text); span.className = "topup"; } obj.nodes["Title-node"] && /*OverDrive.createGlobalWishlistCheckbox*/NoOp(sp.createOtherServiceLinks(obj.nodes["Title-node"], obj, "title", true), obj); break; } } return obj; }); new Service("marketplace", "OverDrive Marketplace", "MP", ["marketplace.overdrive.com"], { title:{href:"https://marketplace.overdrive.com/TitleDetails?crid={id}", fields:["id"],}, }, function(pathname){ let sp = this; //let pathsplit = pathname.split("/"); //let bit = pathsplit[1].toLowerCase(); //log(bit); let obj; $Z("//a[starts-with(@href,'/TitleDetails?crid=')]",function(a){ if(a.textContent.trim()){//no image links! insertAfter(document.createTextNode(" "), a); sp.createOtherServiceLinks(a.parentNode, objFilter(search2obj(a.search, true), {"id":"crid"}), "title"); } }); }) new Service("cheater", "Rotten Tomatoes", "RT", [], { title:{href:"http://www.rottentomatoes.com/search/?search={title}", fields:{"title":((t) => ((t.Formats && t.Formats.some((f) => (f == 635 || f == 35)))?t.title:undefined)),},}, }, function(){}) return od; //http://videosamples.overdrive.com/?crid=8683ED4D-9364-4CDA-9BF6-F486AD388710&themeprofile=DLR-78&retailerid=LosAngeles //http://videosamples.overdrive.com/?crid=8683ED4D-9364-4CDA-9BF6-F486AD388710 }); lazyProperty(this, "CountryCodes", function(){//ISO-3166-1993 return {//https://www.iso.org/obp/ui/#search/code/ "AF":"Afghanistan", "AX":"Ã…land Islands", "AL":"Albania", "DZ":"Algeria", "AS":"American Samoa", "AD":"Andorra", "AO":"Angola", "AI":"Anguilla", "AQ":"Antarctica", "AG":"Antigua and Barbuda", "AR":"Argentina", "AM":"Armenia", "AW":"Aruba", "AU":"Australia", "AT":"Austria", "AZ":"Azerbaijan", "BS":"Bahamas (the)", "BH":"Bahrain", "BD":"Bangladesh", "BB":"Barbados", "BY":"Belarus", "BE":"Belgium", "BZ":"Belize", "BJ":"Benin", "BM":"Bermuda", "BT":"Bhutan", "BO":"Bolivia, Plurinational State of", "BQ":"Bonaire, Sint Eustatius and Saba", "BA":"Bosnia and Herzegovina", "BW":"Botswana", "BV":"Bouvet Island", "BR":"Brazil", "IO":"British Indian Ocean Territory (the)", "BN":"Brunei Darussalam", "BG":"Bulgaria", "BF":"Burkina Faso", "BI":"Burundi", "CV":"Cabo Verde", "KH":"Cambodia", "CM":"Cameroon", "CA":"Canada", "KY":"Cayman Islands (the)", "CF":"Central African Republic (the)", "TD":"Chad", "CL":"Chile", "CN":"China", "CX":"Christmas Island", "CC":"Cocos (Keeling) Islands (the)", "CO":"Colombia", "KM":"Comoros", "CG":"Congo", "CD":"Congo (the Democratic Republic of the)", "CK":"Cook Islands (the)", "CR":"Costa Rica", "CI":"Côte d'Ivoire", "HR":"Croatia", "CU":"Cuba", "CW":"Curaçao", "CY":"Cyprus", "CZ":"Czech Republic (the)", "DK":"Denmark", "DJ":"Djibouti", "DM":"Dominica", "DO":"Dominican Republic (the)", "EC":"Ecuador", "EG":"Egypt", "SV":"El Salvador", "GQ":"Equatorial Guinea", "ER":"Eritrea", "EE":"Estonia", "ET":"Ethiopia", "FK":"Falkland Islands (the) [Malvinas]", "FO":"Faroe Islands (the)", "FJ":"Fiji", "FI":"Finland", "FR":"France", "GF":"French Guiana", "PF":"French Polynesia", "TF":"French Southern Territories (the)", "GA":"Gabon", "GM":"Gambia (The)", "GE":"Georgia", "DE":"Germany", "GH":"Ghana", "GI":"Gibraltar", "GR":"Greece", "GL":"Greenland", "GD":"Grenada", "GP":"Guadeloupe", "GU":"Guam", "GT":"Guatemala", "GG":"Guernsey", "GN":"Guinea", "GW":"Guinea-Bissau", "GY":"Guyana", "HT":"Haiti", "HM":"Heard Island and McDonald Islands", "VA":"Holy See (the) [Vatican City State]", "HN":"Honduras", "HK":"Hong Kong", "HU":"Hungary", "IS":"Iceland", "IN":"India", "ID":"Indonesia", "IR":"Iran (the Islamic Republic of)", "IQ":"Iraq", "IE":"Ireland", "IM":"Isle of Man", "IL":"Israel", "IT":"Italy", "JM":"Jamaica", "JP":"Japan", "JE":"Jersey", "JO":"Jordan", "KZ":"Kazakhstan", "KE":"Kenya", "KI":"Kiribati", "KP":"Korea (the Democratic People's Republic of)", "KR":"Korea (the Republic of)", "KW":"Kuwait", "KG":"Kyrgyzstan", "LA":"Lao People's Democratic Republic (the)", "LV":"Latvia", "LB":"Lebanon", "LS":"Lesotho", "LR":"Liberia", "LY":"Libya", "LI":"Liechtenstein", "LT":"Lithuania", "LU":"Luxembourg", "MO":"Macao", "MK":"Macedonia (the former Yugoslav Republic of)", "MG":"Madagascar", "MW":"Malawi", "MY":"Malaysia", "MV":"Maldives", "ML":"Mali", "MT":"Malta", "MH":"Marshall Islands (the)", "MQ":"Martinique", "MR":"Mauritania", "MU":"Mauritius", "YT":"Mayotte", "MX":"Mexico", "FM":"Micronesia (the Federated States of)", "MD":"Moldova (the Republic of)", "MC":"Monaco", "MN":"Mongolia", "ME":"Montenegro", "MS":"Montserrat", "MA":"Morocco", "MZ":"Mozambique", "MM":"Myanmar", "NA":"Namibia", "NR":"Nauru", "NP":"Nepal", "NL":"Netherlands (the)", "NC":"New Caledonia", "NZ":"New Zealand", "NI":"Nicaragua", "NE":"Niger (the)", "NG":"Nigeria", "NU":"Niue", "NF":"Norfolk Island", "MP":"Northern Mariana Islands (the)", "NO":"Norway", "OM":"Oman", "PK":"Pakistan", "PW":"Palau", "PS":"Palestine, State of", "PA":"Panama", "PG":"Papua New Guinea", "PY":"Paraguay", "PE":"Peru", "PH":"Philippines (the)", "PN":"Pitcairn", "PL":"Poland", "PT":"Portugal", "PR":"Puerto Rico", "QA":"Qatar", "RE":"Réunion", "RO":"Romania", "RU":"Russian Federation (the)", "RW":"Rwanda", "BL":"Saint Barthélemy", "SH":"Saint Helena, Ascension and Tristan da Cunha", "KN":"Saint Kitts and Nevis", "LC":"Saint Lucia", "MF":"Saint Martin (French part)", "PM":"Saint Pierre and Miquelon", "VC":"Saint Vincent and the Grenadines", "WS":"Samoa", "SM":"San Marino", "ST":"Sao Tome and Principe", "SA":"Saudi Arabia", "SN":"Senegal", "RS":"Serbia", "SC":"Seychelles", "SL":"Sierra Leone", "SG":"Singapore", "SX":"Sint Maarten (Dutch part)", "SK":"Slovakia", "SI":"Slovenia", "SB":"Solomon Islands (the)", "SO":"Somalia", "ZA":"South Africa", "GS":"South Georgia and the South Sandwich Islands", "SS":"South Sudan", "ES":"Spain", "LK":"Sri Lanka", "SD":"Sudan (the)", "SR":"Suriname", "SJ":"Svalbard and Jan Mayen", "SZ":"Swaziland", "SE":"Sweden", "CH":"Switzerland", "SY":"Syrian Arab Republic (the)", "TW":"Taiwan (Province of China)", "TJ":"Tajikistan", "TZ":"Tanzania, United Republic of", "TH":"Thailand", "TL":"Timor-Leste", "TG":"Togo", "TK":"Tokelau", "TO":"Tonga", "TT":"Trinidad and Tobago", "TN":"Tunisia", "TR":"Turkey", "TM":"Turkmenistan", "TC":"Turks and Caicos Islands (the)", "TV":"Tuvalu", "UG":"Uganda", "UA":"Ukraine", "AE":"United Arab Emirates (the)", "GB":"United Kingdom (the)", "US":"United States (the)", "UM":"United States Minor Outlying Islands (the)", "UY":"Uruguay", "UZ":"Uzbekistan", "VU":"Vanuatu", "VE":"Venezuela, Bolivarian Republic of ", "VN":"Viet Nam", "VG":"Virgin Islands (British)", "VI":"Virgin Islands (U.S.)", "WF":"Wallis and Futuna", "EH":"Western Sahara*", "YE":"Yemen", "ZM":"Zambia", "ZW":"Zimbabwe", }; }); //log(OverDrive); //log(Formats); //log(Providers) //log(Cookies); lazyProperty(this, "globalSettings", function(){ function Setting(name, generator, defaultValue, serialize, deserialize){ this.name = name; this.generator = generator; this.serialize = serialize || JSON.stringify; this.deserialize = deserialize || JSON.parse; let item = this; let obj = { enumerable: true, configurable: false, set: function(value){ // log("set", item.name, value, item.serialize(value)) // if(typeof(value) === "object") // delete item.__proto__; GM_setValue(item.name, item.serialize(value)) }, get: function(){ let raw = GM_getValue(item.name); if(raw !== undefined) { raw = item.deserialize(raw); if(typeof(raw) === "object") raw.__proto__ = defaultValue; } else if(typeof(defaultValue) === "object") raw = {__proto__:defaultValue}; else raw = defaultValue; // log(raw) return raw; }, }; Object.defineProperty(settings, item.name, obj); Object.defineProperty(item, "value", obj); settings.complex.push(this); } let settings = { complex:[], createConfigInterface:function(){ let node = document.createElement("div"); this.complex.forEach(function(item){let temp = document.createElement("div"); node.appendChild(item.generator(temp) || temp);}); return node; }, }; new Setting("serviceLinks", function(node){ let item = this; let value = this.value; node.appendChild(document.createTextNode("For which databases should links be displayed?")); node.appendChild(document.createElement("br")); let changed = false; for each(service in OverDrive.Services) { let name = service.service node.appendChild(textcheckbox(service.title, "service-"+name, function(e){let temp = item.value; temp[name] = e.currentTarget.checked; item.value = temp;}, value[name])); node.appendChild(document.createElement("br")); } }, (function(){let def = {}; for(let n in OverDrive.Services) def[n] = true; return def;})() ); return settings; }); lazyProperty(this, "globalWishlist", function(){ return { obj2item: function(obj){return obj.id.toLowerCase().replace(/[{}]+/g,""); }, item2obj: function(item){return objFilter(item.split(":"), {id:0,format:1});}, add: function(obj){ //Array.map(arguments, globalWishlist.obj2item) let id = globalWishlist.obj2item(obj); let v = GM_getValue("wishlist") || ""; if(!v.length) GM_setValue("wishlist", id); else { let p = v.indexOf(id); if(p == -1) GM_setValue("wishlist", v+","+id); else if(v.length > p + id.length && v[p + id.length] == ":") GM_setValue("wishlist", v.slice(0, p + id.length) + v.slice((v+",").indexOf(",", p))); } }, remove: function(obj){ let v = GM_getValue("wishlist") || ""; if(v.length) { v = "," + v; let id = globalWishlist.obj2item(obj); let p = v.indexOf(","+id); if(p) GM_setValue("wishlist", (v.slice(0, p) + v.slice((v+",").indexOf(",", p + id.length))).slice(1)); } }, has: function(obj) { return (","+(GM_getValue("wishlist") || "")+",").indexOf(","+globalWishlist.obj2item(obj)+",") > -1; }, get items() { return (value = (GM_getValue("wishlist") || ""))?value.split(",").map(globalWishlist.item2obj):[]; }, }; }); /*lazyProperty(this, "languages", function(){ return { "default":"en", en:{ llp_checkout_max:{"starts-with":"You can check out up to ", regexp:/You can check out up to ([1-9][\d]*) titles./}, //Up to 5 titles can be placed on hold at one time. We'll send you an email when a title becomes available. You have 5 days to check out your hold after we email you notice that it is available. llp_hold_max:{"contains":"titles can be placed on hold at one time. We'll send you an email when a title becomes available. You have", regexp:/Up to ([1-9][\d]*) titles can be placed on hold at one time. We'll send you an email when a title becomes available. You have ([1-9][\d]*) days to check out your hold after we email you notice that it is available./}, llp_cart_max:{"contains":" titles at a time. Titles are removed from your ", regexp:/Your [\S]+ will hold up to ([1-9][\d]*) titles at a time. Titles are removed from your [\S]+ after ([1-9][\d]* (?:minute|hour|day|week|month|year)s?) so that other users can have the chance to check them out./}, cart_max:{"starts-with":"You can have up to ", regexp:/You can have up to ([1-9][\d]*) titles in your /}, holds_max:{"starts-with": "You can place up to", regexp: /You can place up to ([1-9][\d]*) titles on hold at a time. /}, items_max:{"starts-with":"You can have up to", regexp: /You can have up to ([1-9][\d]*) titles checked out at any given time./}, hold_expire:{"contains": "The item will be held for you for"}, checkout_max:{"starts-with":"Our system allows you to put up to"}, cart_timeout:{"contains":"it will remain there for", regexp:/it will remain there for ([1-9][\d]* (?:minute|hour|day|week|month|year)s?) before it is automatically removed./}, formats:{"starts-with":"To review system requirements and download free software for your preferred format, click below:"}, patron:{"starts-with":"You are patron ", regexp:/You are patron ([1-9][\d]*) out of ([1-9][\d]*) on the waiting list\./}, notified:{"starts-with":"Email notification sent: ", regexp:/^Email notification sent: (.*)$/}, 1330:{"starts-with:":"We're sorry, but you have reached the holds limit of"}, library_copies:{"contains":"Library copies owned: "}, }, search_pattern:/[\s]*([\S]+)/y, generalValueReader: function(obj, text, pos, regexp){ var regexp = new RegExp(regexp || this.search_pattern); regexp.lastIndex = pos; var m = regexp.exec(text); if(m) return objMerge(m, obj); return; }, getValue: function(name, base, _lang){ var obj = this.getValueObj(name, base, _lang); return obj && ((obj.length>2)?Array.slice(obj,1):obj[1]); }, getValueObj: function(name, base, _lang) { let data = this[_lang || lang || this["default"]][name]; if(base == null || typeof(base.length) === "undefined") base = [base]; if(data) { data = (data.join && data) || [data]; for(let tn in data) { let test = data[tn]; for(let func in test) { if(func == "requires" || func == "regexp" || func == "parse") continue; let d = test[func]; let search = (d.join && d) || [d]; for(let sn in search) { let str = search[sn]; let f = compileXPath("descendant-or-self::text()["+func+"(.,\""+str+"\")]"); for each(let b in base) //if(node = $X("descendant-or-self::text()["+((func === "equals")?"(. = ":(func+"(., "))+"'"+str+")']", b)) { for each(let node in $W(f, b)) { let pos = -1; let text = node.nodeValue; let obj = { "node":node.parentNode, "text":node}; // log(arguments, func, str, b) if(test.parse) obj = test.parse(obj, func, str, name); else if(test.regexp || 0 <= (pos = text.indexOf(str))) obj = this.generalValueReader(obj, text, (0 <= pos)?(pos + str.length):0, test.regexp); if(obj !== undefined) { if(test.requires) { let r = test.requires; let ra = (r.join && r) || [r]; for(let xn in ra) if($X(ra[xn], node.parentNode)) return obj; } else return obj; } } } } } } return; }, }; });*/ /* waiting = { <format:id> {providers: [ {server, copies, queue-position, queue-length}, ] } wishlist = { <format:id> } providers = {<provider>:{state, name, features:{}, formats:[]},} */ function ListPage(top, config, table){ let args = Array.slice(arguments, 3); let parse = function(table){ let objs = []; for(var i = 0; i<args.length; i++){ switch(typeof(args[i])) { case "object": if(!(itemFinder instanceof String)) continue; case "string": $Z(compileXPath(args[i]), function(r, j, func, out){ if(!Array.isArray(func)){ func = [func]; } for(var f of func){ let obj = f.apply(this, arguments); if(obj !== undefined) out.push(obj); } }, table, args[++i], objs, config.provider); break; case "function": args[i](table, objs, config.provider); } } return objs; }; let page = parse(table); if(page.length) { let cf = new CollectionFiltering(top || insertBefore(document.createElement("div"), table), config, page); if(config.paging) document.addEventListener("AutoPagerAfterInsert", function (e){ let out = cf.add(/**/log/**/(parse(e.target)), e.target); if(config.pagingCleanup) { config.pagingCleanup(out); } }, true); } return page; } var sp = OverDrive.getService(server, location.pathname) if(sp /*&& server_mode != "letter"/**/) { log(sp.service); //ye gods they redid their website x_x //at least i can query it as to which libraries it supports. //idea: make a reading template for each page. let orderbyrow = $X("//comment()[starts-with(.,'-SEARCH RESULTS PAGE')]/following-sibling::table[1]/tbody/tr[1]");//$X("//nobr[text()='Order By:']/ancestor::table[1]/ancestor::tr[1]"); if(orderbyrow) { //find table and cell height attribute and remove it. let target = document.createElement("tr"); let row = document.createElement("tr"); cell = document.createElement("td"); cell.colSpan = 2; row.appendChild(cell); insertAfter(row, orderbyrow); } //this is ok because server_mode is in this scope legitmately. let cf; let cell; GM_addStyle([//{ "body > * { margin-left:auto; margin-right:auto; }", "a.subscription-active {background-color: rgba(0,255,0, 0.5); white-space:pre; }", "a.subscription-inactive {background-color: rgba(255,140,0, 0.5); white-space:pre; }", ".smaller { font-size:70%; }", ".up { vertical-align:super; }", ".sup { font-size:70%; vertical-align:super; padding-left:1px; }", ".topup { font-size:70%; vertical-align:top; padding-left:1px; }", ".titleDetails { width:auto; }", // "a.subscription-none {background-color: rgba(255,0,0, 0.25);}" ].join("\n"));//} {//page process let obj = sp.parse(location.pathname); /**/ if(obj){ if(obj.serial) log(obj, obj.serial()); else if(isArray(obj)) log(obj, obj.map(function(o){return o.serial();})); else log(obj); /**/ } /**/ LinkAdjust(null, null, sp); } imageResizing(); } else { //This code needs to be entirely rewritten. var dobj; var page; let valid = false; let dcs = $X("//noscript[contains(text(), 'id=\"DCSIMG\"')]"); let img; if(dcs && (img = $X(".//img", evalNoScript(dcs.textContent, document.createElement("div"))))){ dcs = search2obj(url2location(img.src).search, true); log(dcs); if(dcs.dcssip && dcs["dcsext.websiteid"] ){ Providers.addAlias(server, dcs.dcssip.toLowerCase()); valid = true; } } var ContentBase = $X("//*[comment()["+ [".=' ************************* BEGIN PAGE CONTENT ************************* '", ".=' ************************** END PAGE CONTENT ************************** '"].join(" or ")+ "] or table[@class='breadCrumbs'] or a[@name='maincontent'] or @class='mainActiveContent']"); let secure_servers; if((location.protocol == "https:") && (secure_servers = /^[^.]+\.(libraryreserve|mediavending)\.com$/.exec(server))) {//it could be a config page ^_^ var rConfig = /\/([^\/]+)\/([^.\/]+)\.htm$/; let m = rConfig.exec(location.pathname.toLowerCase()); if(m) { //1 is the lang //2 is the page //TODO Add support for inactive libraries. //server = m[2]; lang = m[1]; page = m[2]; if(page.lastIndexOf("signin", 0) == 0 || page.lastIndexOf("login", 0) == 0) { let nodes; var form = $X("//form[@action='BANGAuthenticate.dll']") //Array.filter(document.forms, function(f){return f.getAttribute("action") == "BANGAuthenticate.dll";})[0]; // if(!form) return; var last = null; var before = 0; var after = 0; var old_tr = null; if(form) { var submit = $X(".//input[@type='submit' or @value='Sign In' or @alt='Sign In'] | .//div[@class='signinbutton'] | .//a[contains(text(), 'lease click here to sign into this system.')]", form); // if(!submit) return; var node = submit || Array.slice(form.elements, -1)[0]; var td = $X("ancestor::td[1]", node); if(td) { old_tr = $X("ancestor::tr[1]", td); before = td.cellIndex; after = old_tr.cells.length - before - 1; } last = $X("../tr[last()]", old_tr) || node; } else { var base = $X("//*[p[text()='Access to this site is limited to library patrons only.']]") || ContentBase; if(!base) log(location); else last = base.lastChild; } if(last) { dobj = Providers.getProvider(server, true); let obj = {}; // log(dobj) // log(ser); //TODO fix this so it works better on ebooks.carinapress.com var tr = document.createElement("tr"); for(var i = 0; i < before; i++) { let td = tr.appendChild(document.createElement("td")); if(old_tr) td.style.cssText = old_tr.cells[i].style.cssText; } var td = tr.appendChild(document.createElement("td")); ++before; for(var i = 0; i < after; i++) { let td = tr.appendChild(document.createElement("td")); if(old_tr) td.style.cssText = old_tr.cells[i + before].style.cssText; } switch(secure_servers[1]){ case "libraryreserve": // lcheckbox.appendChild(document.createTextNode(" Include this library")); obj.type = "library"; obj.secure = secure_servers[1]; break; case "mediavending": // lcheckbox.appendChild(document.createTextNode(" Include this store")); obj.type = "store"; obj.secure = secure_servers[1]; break; } radio("state", dobj.mode, ["Include", "Inactive", "Exclude"], function(e){Providers.storeProvider(server, e.details.value);}, td); td.appendChild(document.createElement("div").appendChild(globalSettings.createConfigInterface())); insertAfter(tr, last); let server_obj = dobj.data; let dirty = false; for(prop in obj) { if(server_obj[prop] != obj[prop]) { server_obj[prop]= obj[prop]; dirty = true; } } if(dirty) dobj.data = server_obj; } } } } if(!dobj) dobj = Providers.getProvider(server, true); if(valid || dobj.mode != "Exclude" || dobj.host != server || (/\.(?:(?:lib|ret)\.overdrive|libraryreserve)\.com$/.test(server))) { var signed_in = false; if($X("//a[@href='BANGAuthenticate.dll?Action=SignOut&URL=SignOutConfirm.htm']")) signed_in = true; if($X("//a[@href='BANGAuthenticate.dll?Action=AuthCheck&URL=MyAccount.htm&ForceLoginFlag=']")) signed_in = false; log("parsed - " + dobj.parse(page, location, signed_in)); if(location.hash != "#embedded") { /*//for SearchResults && ContentDetails //DOES NOT WORK $Z("//script[contains(text(), 'javascript:addtoWishList')]", function(r,i,p){ var hmmm = $X(compileXPath("./preceding-sibling::a[1]"),r); if(hmmm) { switch(search2obj(hmmm.search, true).action) { case "Add": hmmm.innerHTML="add to cart"; break; case "AuthCheck": hmmm.innerHTML="join waiting list"; break; } var a = $X(compileXPath("./following-sibling::a[1]"),r); if(a) a.innerHTML="add to wish list"; } }); */ /*{//FullText search formats var groups = []; let server_obj = dobj.data; //log(server_obj) let test = server_obj && server_obj.formats && function(f){ return server_obj.formats.indexOf(f) > -1; }; for(a in Formats.Classes) { let cls = Formats.Classes[a]; let formats = test?cls.formats.filter(test):cls.formats.slice(); if(formats.length > 0) { formats.title = cls.title + "s"; groups.push(formats); } } $Z("//form[@action='BANGSearch.dll?Type=FullText']//select[@name='Format']", function(select, i){ //log(arguments) let test = []; Array.slice(select.options, 0). map(function(f){ let m = (f.value && f.value.split(",").map(function(a){return Number(a);}).sort(function(a, b){return a - b;})) || []; let v = m.length?m:[0]; f.parentNode.removeChild(f); groups = groups.filter(function(u){return !array_equals(v,u);});//log(v, u, !compare(v,u)); return f; }). concat(groups.map(function(n){ //log(n); let o = document.createElement("option"); o.value = n.join(","); o.appendChild(document.createTextNode(n.title)); return o; })). // map(function(m){test.push(m.value); return m;}). sort(function(a, b){ if(!a.value) return -1; else if(!b.value) return 1; else return (a.textContent > b.textContent) - (a.textContent < b.textContent); }). forEach(select.appendChild); //log(test); //nodes. select.selectedIndex=0; }); } */ /*{//Set my PerPage for all searches. for(var [name, value] in Iterator({form:"action", a:"href", button:"formaction"})) $Z("//"+name+"[starts-with(@"+value+", 'BANGSearch.dll?') and not(contains(@"+value+", '&PerPage='))]", function(node){ node[value] += "&PerPage="+QuickSearchsCount;}) }*/ dobj.housekeeping(page, signed_in); makeFloater(dobj, ContentBase); /*//some libraries have font resizing, this refreshes the font resizer. It's a bit of a hack. if(typeof unsafeWindow.dw_fontSizerDX === "function") unsafeWindow.setTimeout ( "if(dw_fontSizerDX){dw_fontSizerDX.curSize -= 1; dw_fontSizerDX.adjust(1);}", 1 ); */ } } else throw "done"; } //Image Resizing GM_addStyle([//{ //"a.reformed-link { display:none; }", // "[parsed] nobr { white-space: inherit; }", "div#dj, div#wrapper, div.results_title, div#title_bookshelf, div#bookshelf_left { width:auto; }", ".item-duplicate { display:none; }", "div[reviews='name'] i br { display:none; }",//superflous br, just easier to hide it ".cleaned br.hide-break { display:none; }", ".renumbered span.creator, .renumbered span.track-title { font-size:inherit; }", ".renumbered .track-number { width:2.65ch; display:inline-block; }", ".strike {text-decoration: line-through;}" , "form.reformed-link { display:inline; position:absolute; opacity: 0; overflow:hidden; }", 'form.reformed-link > input[type="submit"] {'+ '-moz-appearance: inherit; font:inherit; '+ 'cursor:pointer; '+ 'border-style:none; '+ 'border-width:0; '+ 'padding: 0; '+ 'position: relative; '+//right:3px; bottom:1px;'+//left: 0px; top: 0px; // 'white-space:nowrap; '+ '}', // "a.reformed-link { white-space:nowrap; }", "div.reformed-link { white-space:nowrap; display:inline-block; margin:0; font-size:1em;}", "div.reformed-link:hover { text-decoration:underline; }", "a.javascript-submit-link form { display:none; }", ".field-abridged { background-color: rgba(255,0,0,0.25); }", ".field-unabridged { background-color: rgba(0,0,255,0.25); }", "img.real-size { position:absolute; z-index:1; }", "img.real-size:hover, img.real-size.bludgen { width:auto!important; height:auto!important; z-index:2; }", "img.real-size.bludgen { display:none; }", ".nowrap { white-space:pre; }", ".format_container_left { width: 510px!important; }", ".format_info_right { width: 350px!important; }", "#formats-unknown { background-color:rgba(255,0,0,0.5);}", ].join("\n\n"));//} function LinkAdjust(root, objs, sp, modern) { //I think this function may be dead. return; $Z(compileXPath(".//a[starts-with(translate(@href, ' ', ''), 'javascript:')]"), function(a){ if(a.protocol == "javascript:") { //javascript: TitleInfo('{A6AAE8CE-F944-44F3-960D-4E9A7715CDD5}', 25); //http://search.overdrive.com/TitleInfo.asp?TitleReserveID=A6AAE8CE-F944-44F3-960D-4E9A7715CDD5&TitleFormatID=25 //http://search.overdrive.com/TitleInfo/A6AAE8CE-F944-44F3-960D-4E9A7715CDD5 //http://search.overdrive.com/ti/A6AAE8CE-F944-44F3-960D-4E9A7715CDD5 var m = ReadJavascriptHref(a, "href"); if(m.success) { var params = m.params; let sod, cr, /*search_classic,*/ search_modern; var text = a.textContent.trim(); if(text.indexOf(" ") >= 0) text = '"'+text + '"'; let spn = sp || OverDrive.Services[a.getAttribute("service") || ""]; switch(m.name) { case "TitleInfo":{ //THIS IS THE WRONG PLACE TO DO THIS!!! spn.overwriteLink(a, "title", params); if(!$X(compileXPath("./*"),a)){ let s = document.createElement("sup"); s.appendChild(document.createTextNode(" ")); let tobj = {"id":params[0],"format":params[1]}; insertAfter(OverDrive.createGlobalWishlistCheckbox(sp.createOtherServiceLinks(s, tobj, undefined, true), tobj), a); } break;} case "SearchCreator":{ //search_classic = {"CreatorID": params[0]}; search_modern = {"Creator":text} break;} case "SearchPublisher":{ //search_classic = {"lpubPublisherName": params[0].replace(/[,\.]/g, "")}; search_modern = {"Publisher":text} break;} case "SearchImprint":{ //search_classic = {"limpImprintName": params[0]}; search_modern = {"Imprint":text} break;} case "SearchSeries":{ //search_classic = {"SeriesReserveID": params[0]}; search_modern = {"Series":text} break;} }/* if(search_classic) { //failing to update "a" results in modern links being injected inside the box for the reformed link. let na = spn.overwriteLink(a, "search", search_classic); a = na; }*/ /**/ if(modern != false && search_modern && OverDrive.Services.modern) { var link = OverDrive.Services.modern.createLink("search", search_modern, document.createTextNode("M")); if(link){ var span = document.createElement("span") //span.appendChild(document.createTextNode("[")); span.appendChild(link); //span.appendChild(document.createTextNode("]")); span.className = "sup" insertAfter(span, a); } }/**/ } } }, root); } function imageResizing(root) {//extra code for all valid pages function mouseout(){ if(this.style.length > 1) this.style.removeProperty("display"); else this.removeAttribute("style"); } //Different servers different locations //100 is the largest, then 400, then 150 then 200. I think 100 may be the orig size //http://www.contentreserve.com/TitleInfo.asp?ID={471D9370-BFBB-4BE3-88DD-8CE0D0655123}&Format=25 //https://search.overdrive.com/media/1372643/how-to-train-your-dragon //https://origin-images.contentreserve.com/ImageType-100/0887-1/%7B471D9370-BFBB-4BE3-88DD-8CE0D0655123%7DImg100.jpg //https://images.contentreserve.com/ImageType-400/0887-1/471/D93/70/%7B471D9370-BFBB-4BE3-88DD-8CE0D0655123%7DImg400.jpg var urls = [//{ // "https?://origin-images.contentreserve.com/img/ImageType-100/", <--- highest quality // "https?://origin-images.contentreserve.com/img/ImageType-150/",//seems to be dead // "https?://origin-images.contentreserve.com/img/ImageType-200/", <--- tiny // "https?://origin-images.contentreserve.com/img/ImageType-400/",//seems to be dead "https://origin-images.contentreserve.com/ImageType-",//100 and 200 "http://origin-images.contentreserve.com/ImageType-",//100 and 200 // "https://secure.contentreserve.com/img/ImageType-100/",//seems to be dead // "https://secure.contentreserve.com/img/ImageType-150/", // "https://secure.contentreserve.com/img/ImageType-200/",//seems to be dead // "https://secure.contentreserve.com/img/ImageType-400/", "https://secure.contentreserve.com/img/ImageType-",//150 and 400 // "http://images.contentreserve.com/ImageType-100/", //seems to be dead // "http://images.contentreserve.com/ImageType-150/", // "http://images.contentreserve.com/ImageType-200/",//seems to be dead // "http://images.contentreserve.com/ImageType-400/", "http://images.contentreserve.com/ImageType-", //150 and 400 "GetImage.asp?", "http://images.booksamillion.com/covers2/ebook/ImageType-",//100 & 200 ];//} $Z(compileXPath(".//img[not(@resized or contains(concat(' ', @class, ' '), ' lrgImg ')) and ("+urls.map(function(f){return "starts-with(@src, '"+f+"')"}).join(" or ")+")]"), function(r){ //de-alt the images (alts have modified specific aspect ratio) r.src = r.src.replace(/^(http(?:\:\/\/(?:images|origin-images)|s\:\/\/(?:secure|origin-images))\.contentreserve\.com\/[^?]*?)-alt(.jpg)$/, "$1$2"); r.setAttribute("resized", true); var image = r.cloneNode(true); image.classList.add("real-size"); // image.height = image.naturalHeight; // image.width = image.naturalWidth; r.parentNode.insertBefore(image, r); if($X(compileXPath("ancestor::div[contains(concat(' ', @class, ' '), ' browser-slider ')]"), r)) { //image.removeAttribute("height"); //image.removeAttribute("width"); image.classList.add("bludgen"); addEvent( r, "mouseover", function(){ image.style.display = "block"; let cs = window.getComputedStyle(image,null);//boarder width counts against margin image.style.marginLeft = ( findPos(r)[0] - findPos(image)[0] - Number(cs.borderLeftWidth.slice(0,-2)) + Number(cs.marginLeft.slice(0,-2)) ) + "px"; }); addEvent( image, "mouseout", mouseout); } /* //no reliable yet if(r.naturalHeight < r.height && (p = image.src.indexOf("ImageType-"))) {//sometimes the high and low defintion images have been confused. log(p, image.src[p + 10]) image.src = image.src.replace(/(ImageType-|Img)[12]00/g, {1:"$1200", 2:"$1100"}[image.src[p + 10]]) log(p, image.src[p + 10]) }*/ }, root); } //{Object Stuff function WrapNewArray(){return [];} function WrapNewObject(){return {};} function isArray(array){ return array && ((array instanceof Array) || (array.hasOwnProperty && array.hasOwnProperty("length") && !array.propertyIsEnumerable("length") && typeof(array.length) === "number" && array.length >= 0)); } function isArrayIterable(that){ for(var i = 0; i < that.length; i++) if(that.hasOwnProperty(i)) return that.propertyIsEnumerable(i); return !that.length; } function cloneObj(that, skip, cloneNodeDeep, func) { let obj; let skip = skip || {}; let fields = []; switch(typeof(that)) { case "number": case "boolean":{//case "string": return that;//can't have properties } case "string":{ return that.toString();//generates a new string } case "xml":{ //not an object! no __proto__, no constructor //if a.toXMLString() == b.toXMLString() than a == b but a === b will only be true if they are the same instance. return new XML(that);//this creates a new instance. //XML objects use namespaces, it has a "function" namespace } case "object":{ if (that.cloneNode) return (cloneNodeDeep != null)?that.cloneNode(cloneNodeDeep):that; switch(that.constructor){ case String: case Number: case Boolean: case Date: case RegExp: case Function: obj = new that.constructor(that); break; default: if(isArray(that)){ obj = new Array(that.length); if(!isArrayIterable(that))//workaround for arguments and other non arrays types that can't be enumerated for(var i = 0; i < that.length; i++) if(skip[i] !== null) fields.push(i); } else {//fake it! obj = {}; obj.__proto__ = that.__proto__; if(that.hasOwnProperty("constructor")) obj.constructor = that.constructor; } } } } if(that.constructor === String){ for(var i = 0, j; i < that.length; i++)//you could iterate over the individual letters, but nothing is gained! if(!skip.hasOwnProperty(i)) skip[i] = null; } fields = [field for (field in that)].concat(fields); for each(let field in fields) { if(skip[field] !== null && (Object.getOwnPropertyDescriptor(that, field) || {writable:undefined}).writable !== undefined){ // log("cloning", field); let fieldvalue = that[field]; switch(typeof(fieldvalue)) { default: if(fieldvalue !== null || field == "__proto__") {//it's a really bad idea to clone __proto__ fields! let value; switch(typeof(func)){ case "string":{ if(typeof(fieldvalue[func]) === "function") value = fieldvalue[func](); break; } case "function":{ value = func.call(fieldvalue); break; } } if(value === undefined) value = cloneObj(fieldvalue, (field in skip)?skip[field]:skip[""], cloneNodeDeep, func); obj[field] = value; break; } case "number": case "boolean": case "undefined": obj[field] = fieldvalue;//can't clone it so copy it. } } } return obj; }; function execWithNameAccessors(regexp, names, parsers){ parsers = parsers || {}; let aliases = {}; let exec = function my(){var out = RegExp.prototype.exec.apply(this, arguments); if(out !== null) out.__proto__ = my.execPrototype; /*/log(out);/**/ return out;}; let proto = /*/ regexp.proto = /**/ exec.execPrototype = Object.create(Object.getPrototypeOf([])); //proto.getGroupValues = function my(name){return aliases.hasOwnProperty(name)?aliases.name.map(function(p){return this[p];}):undefined; } let addAlias = function add(name, position){ if(name != null) { if(aliases.hasOwnProperty(name)) aliases[name].push(position); else { let arr = quickSetValueProperty(aliases, name, [position]); Object.defineProperty(proto, name, { set:function(value){ for each(var p in arr) this[p] = value;}, get:function(){ let value; for(var i = arr.length; i;) if((value = this[arr[--i]]) != null) return (parsers[name] || NoOp)(value); }, enumerable: true, configurable: true, }); } } } /**/ //Allow multiple aliases per location let add = addAlias; addAlias = function(name, position){ switch(typeof(name)) { case "number": case "string": add.apply(this, arguments); break; default: if(Array.isArray(name)) for each(let n in name) add.apply(this, [n].concat(Array.slice(arguments, 1))); } } /**/ // quickSetValueProperty(regexp, "addAlias", addAlias, false); quickSetValueProperty(regexp, "aliases", aliases, false); quickSetValueProperty(regexp, "parsers", parsers, false); quickSetValueProperty(regexp, "exec", exec, false); names.forEach(addAlias); return regexp; } function array_equals(a, b){ if(a == b) return true; if(a.length != b.length) return false; for(var i = 0; i < a.length; i++) if(a[i] !== b[i]) return false; return true; } function objMerge(target){ if(!target) throw new Error("'target' cannot be null"); var sources = Array.slice(arguments, 1); for each(var source in sources) if(source != null) for(v in source) if(source.hasOwnProperty(v)) { if(source[v] !== undefined) target[v] = source[v]; else delete target[v]; } return target; } function nodeMerge(target){ if(typeof target === "string") target = ((target[0] == "!")?document.createTextNode(target.slice(1)):document.createElement(target)); for(var source of Array.slice(arguments, 1)){ if(source instanceof Node) target.appendChild(source); else if(typeof source === "string") target.appendChild(document.createTextNode(source)); else for(var [name, value] in Iterator(source)) if(value !== undefined) { target.setAttribute(name,value); } else { target.removeAttribute(name); } } return target; } function objMap(source, fields, strict, obj, func, all, gfv){ if(typeof(fields) === "boolean"){ fields = fields?Object.keys(source):Object.getOwnPropertyNames(source); strict = false;//strict mode makes no sense. } if(obj == null) obj = {}; if(source == null) return obj; func = func || NoOp; gfv = gfv || GetFieldValue; let temp; if(strict) {//do not remove brackets, or the else will be on the inner if! for(let [to, from] in new Iterator(fields)) if((temp = gfv(source, from, to, obj)) !== undefined)//function(source, from, to, obj) obj[to] = func(temp, source, from, to, obj); else if(all) return false; } else { for(let [to, from] in new Iterator(fields)) if((temp = gfv(source, from, to, obj)) !== undefined)//function(source, from, to, obj) obj[(typeof(to) === "number")?from:to] = func(temp, source, from, to, obj); else if(all) return false; } return obj; } function objFilter(source, fields, obj, func, all, gfv){ return objMap(source, fields, !isArray(fields), obj, func, all, gfv); } function GetFieldValue(obj, name) { if(name != null) { switch(typeof(name)) { case "function": return name.apply(obj, arguments); case "string": case "number": name = [name]; } for each(var n in name) if(typeof(obj[n]) !== "undefined") return obj[n]; } return; } function GetAttributeValue(obj, name) { if(name != null) { switch(typeof(name)) { case "function": return name.apply(obj, arguments); case "string": case "number": name = [name]; } for each(var n in name) if(obj.hasAttribute(n)) return obj.getAttribute(n); } return; } function getPropertyDescriptor(source, name){ let desc; do ; while(!(desc = Object.getOwnPropertyDescriptor(source, name)) && (source = Object.getPrototypeOf(source))); return desc; } function objMapOwn(obj, func){//returns a new object who's properties share the same name as obj but who's values are the result of passing the value to func. let target = {}; let args = Array.slice(arguments) args[1] = obj; Object.getOwnPropertyNames(obj).forEach(function(name, index, array){args[0] = name ; target[name] = func.apply(obj, args);}); return target; } function objCreate(prototype, fields){//Creates a clone of an object but with a different prototype. return Object.create(prototype, fields?objMapOwn(fields, function(name){ return Object.getOwnPropertyDescriptor(fields, name);}):undefined); } function objCreateWithSelfDestructingAccessors(prototype, fields, accessors){ return installSelfDestructingGettersAndSetters(objCreate(prototype, fields), accessors); } function objHasFields(obj, fields){ for each(let from in fields) if(GetFieldValue(obj, from) === undefined) return false; return true; } //} /* function getStringBetween(string, start, end){ return string.slice(start, end); } */ function SmartQuotes(text) { if(text.indexOf("\"") == -1) return "\""+text+"\""; if(text.indexOf("'") == -1) return "'"+text+"'"; throw new Error("You are screwed! The string contains both single AND double quotes\n" + text); } //{Lazy Properties and Self Destructing Accessors function quickSetValueProperty(target, name, value, enumerable){ Object.defineProperty(target, name, {value: value, writable: true, enumerable: enumerable || (enumerable == null), configurable: true}); return value; } function lazyProperties(obj, properties, installPropertyOnInstance, enumerable){//enumerable defaults to true return Object.defineProperties(obj, objMapOwn(properties, installPropertyOnInstance ? function(name, source){ return { set:function(value){ quickSetValueProperty(this, name, value, enumerable); }, get:function(){ return quickSetValueProperty(this, name, source[name].call(this, name), enumerable); }, enumerable: false, configurable: true, };} : function(name, source, obj){ return { set:function(value){ quickSetValueProperty(obj, name, value, enumerable); }, get:function(){ return quickSetValueProperty(obj, name, source[name].call(obj, name), enumerable); }, enumerable: false, configurable: true, };}, obj)); } function lazyProperty(obj, name, generator, installPropertyOnInstance, enumerable){ /*/if(!obj) return generator(); if(name != "Supported") log(obj)/**/ return Object.defineProperty(obj, name, installPropertyOnInstance ? { set:function(value){ quickSetValueProperty(this, name, value, enumerable);}, get:function(){return quickSetValueProperty(this, name, generator.call(this, name), enumerable);}, enumerable: false, configurable: true } : { enumerable: false, configurable: true, set:function(value){ quickSetValueProperty(obj, name, value, enumerable);}, get:function(){return quickSetValueProperty(obj, name, generator.call(obj, name), enumerable);}, }); } function installSelfDestructingGettersAndSetters(obj, details, onlyDestructOnDecendants){ return Object.defineProperties(obj, objMapOwn(details, function(name, source, obj){ return { enumerable: false, configurable: true, set: function(value){ quickSetValueProperty(this, name, value);}, get: function(){ let value = Object.create(source[name].prototype); let temp = source[name].call(value, this); let result = (temp !== null && typeof(temp) === "object")?temp:value; return (onlyDestructOnDecendants && this === obj)?result:quickSetValueProperty(this, name, result); }, }; }, obj)); } //} function doWhile(start, xpath, array){ xpath = compileXPath(xpath); if(!array) array = []; do array.push(start); while(start = $X(xpath, start)); return array; } function NoOp(arg) {return arg;} function emQuote(str){return (String.replace(str, /^(\S+\s\S.*)$/, "\"$1\""));} //{Serialization function obj2post(obj, parameter_names){ var e = function(a, b){return (b !== null)?(encodeURIComponent(a)+"="+encodeURIComponent(b)):"";} return (parameter_names?[e(a, GetFieldValue(obj, parameter_names[a])) for (a in parameter_names)]:[e(a, obj[a]) for (a in obj)]).filter(function(a){return a != ""}).join("&"); } function search2obj(search, forceLowerCaseKeys){ var obj = {}; if(typeof search === "object" && search.search != null) search = search.search; if(search[0] == "?") search = search.substr(1); if(search !=""){ if(forceLowerCaseKeys) for(var a of search.split("&")){ var b = a.indexOf('='); if(b >= 0) obj[decodeURIComponent(a.substr(0, b).toLowerCase())] = decodeURIComponent(a.substr(b + 1)); else obj[decodeURIComponent(a.toLowerCase())] = null; } else for(var a of search.split("&")){ var b = a.indexOf('='); if(b >= 0) obj[decodeURIComponent(a.substr(0, b))] = decodeURIComponent(a.substr(b + 1)); else obj[decodeURIComponent(a)] = null; } } return obj; } function url2location(url, doc){ var a = ((doc && doc.createElement && doc) || document).createElement("a"); a.href = url; return objFilter(a, ["hash","host","hostname","href","pathname","port","protocol","search"]); } function ReadJavascriptHref(a, name){ if(a == null) return; var ret = {success:false}; var split = /^javascript:[\s]*(([A-Za-z_$0-9]+)[\s]*\((.*)\));?$/; var href = a; if(name != null) { ret.node = a; ret.field = name; if(typeof a.getAttribute === "function") href = a.getAttribute(name); else href = a[name]; //if(href === undefined) } else { ret.source = a; } // log(arguments, ret); var m = href && split.exec(href.trim()); if(m) { var params; ret.name = m[2]; ret.raw = m[3]; try { ret.params = eval("function "+m[2]+"(){return arguments;}; "+m[1]); ret.success = true; ret.proxy = function(that){ if(typeof that[ret.name] === "function"){ return function(params){ if(!Array.isArray(params)){ params = ret.params; } that[ret.name].apply(that, params); } } } } catch(e) { ret.error = e; } } return ret; } //} //{HTML Work function findTextElementContaining(text, root, extraXpathTest, postfix, prefix, startsWithNotContains) { let s = false, d = false; if(Array.some(text, function(c){ switch(c){ case "\"": d = true; break; case "\'": s = true; break; } return s && d; })) {//bloody hell it uses both if(startsWithNotContains){ if(postfix) { let f = compileXPath(postfix); for(let node in $Y((prefix!=null?prefix:"descendant-or-self::")+"text()["+postfix+"]" + (extraXpathTest?"["+extraXpathTest+"]":""), root)) if(node.nodeValue.lastIndexOf(text, 0) == 0) return $X(f, node); } else for(let node in $Y((prefix!=null?prefix:"descendant-or-self::")+"text()" + (extraXpathTest?"["+extraXpathTest+"]":""), root)) if(node.nodeValue.lastIndexOf(text, 0) == 0) return node; } else { if(postfix) { let f = compileXPath(postfix); for(let node in $Y((prefix!=null?prefix:"descendant-or-self::")+"text()["+postfix+"]" + (extraXpathTest?"["+extraXpathTest+"]":""), root)) if(node.nodeValue.indexOf(text) > -1) return $X(f, node); } else for(let node in $Y((prefix!=null?prefix:"descendant-or-self::")+"text()" + (extraXpathTest?"["+extraXpathTest+"]":""), root)) if(node.nodeValue.indexOf(text) > -1) return node; } } else if(s) return $X((prefix!=null?prefix:"descendant-or-self::")+"text()["+(startsWithNotContains?"starts-with":"contains")+"(., \""+text+"\")]" + (extraXpathTest?"["+extraXpathTest+"]":"")+(postfix?"/"+postfix:""), root); else// if(d) return $X((prefix!=null?prefix:"descendant-or-self::")+"text()["+(startsWithNotContains?"starts-with":"contains")+"(., '"+text+"')]" + (extraXpathTest?"["+extraXpathTest+"]":"")+(postfix?"/"+postfix:""), root); return; } function SplitTextNode(node, str, replace){ let p; if(node && node.nodeValue && (p = node.nodeValue.indexOf(str)) > -1) { if(before = node.nodeValue.substring(0, p)) insertBefore(document.createTextNode(before), node); if(after = node.nodeValue.substring(p + str.length)) insertAfter(document.createTextNode(after), node); node.nodeValue = (replace == null)?str:replace; return node; } return; } function findPos(obj) {//http://www.quirksmode.org/js/findpos.html var curleft = 0; var curtop = 0; if (obj.offsetParent) do { curleft += obj.offsetLeft; curtop += obj.offsetTop; } while (obj = obj.offsetParent); return [curleft, curtop]; } function textcheckbox(text, id, click, checked){ var span = document.createElement("span"); let box = document.createElement("input"); let label = document.createElement("label"); box.type = "checkbox"; box.checked = checked || false; box.id = label.htmlFor = id; label.innerHTML = text; span.appendChild(box); span.appendChild(label); if(click) addEvent( box, "click", click); return span; } function radio(name, state, buttons, click, node){ //[{text:"Include", state:true}, {text:"Inactive", state:false}, {text:"Exclude", state:null, set:"Exclude"}] return buttons.map(function (it){ var or = (typeof(it) === "string") && it; let span = document.createElement("span"); let input = span.appendChild(document.createElement("input")); let label = span.appendChild(document.createElement("label")); var m = { raw:it, value:it.value || or, text:it.text || or, id:it.id || it.text || or, state:it.state || it.value || or, span:span, input:input, label:label, }; input.type="radio" input.name=name input.checked = (state == m.state); if(m.value) input.value = m.value; label.appendChild(document.createTextNode(" "+m.text+" ")); label.htmlFor = input.id = "radio-"+m.id; if(click) input.addEventListener("click", function(e){e.details = m; click.call(this, e);}, false); if(node) node.appendChild(span); return m; }); } function smart_buttons_column(cell, buttons){ for(let j = 0; j < buttons.length; j++) { let br = true; let k = buttons[j]; if(k !== null) { if(k.ownerDocument) { cell.appendChild(k); br = false; } else if(k.dropdown) { let display = ""//k.display?" { display:"+k.display+"!important; }":"" let select = k.select = document.createElement("select"); select.id = k.id; k.dropdown.forEach(k.add = function(o){ if(!o.skip){ var option = document.createElement("option"); option.appendChild(document.createTextNode(o.text)); if(o.selector || o.value) option.value = o.selector || o.value || ""; if(o.alt) option.alt = option.title = o.alt if(o.style) option.style = o.style select.appendChild(option); } }); cell.appendChild(select); if(k.event) { addEvent(select, "change", k.event); } else { let style = k.style = GM_addStyle(display); let info = k.important?"{ display:none !important; }":"{ display:none; }"; style.id="style-"+k.id; addEvent(select, "change", function(e){ style.innerHTML = this.value?this.value + info:display; }); } if(k.alt) select.alt = select.title = k.alt; } else if(k.id) { //TODO: id styles so flip can bypass clicking. also store selector in style attributes. br = false; let span = k.span = document.createElement("div"); let box = k.input = document.createElement("input"); let label = k.label = document.createElement("label"); let text = ""; if(k.text) text = k.text; else if(k.title) { text = k.title; label.classList.add("title"); } if("sort" in k) span.setAttribute("sort", k.sort); box.type = "checkbox"; box.checked = true; box.id = label.htmlFor = "toggle-" + k.id + "-" + Math.ceil(Math.random() * 100000); box.title = label.title = label.innerHTML = text; span.classList.add("boxlabel"); span.appendChild(box); span.appendChild(label); cell.appendChild(span); if(k.selector) { let display = ""//k.display?k.selector +" { display:"+k.display+"!important; }":"" let style = GM_addStyle(display); style.id = "style-"+k.id; addEvent( box, "click", function(){ style.innerHTML = box.checked?display:(k.selector +" { display:none; }"); }); label.classList.add("click"); } if(k.flip) { //Using this is a bad idea! //label.classList.add("toggle"); let flip = k.flip; let id = k.id; if(k.flip == "cell") addEvent( box, "click", function(e){ $Z(compileXPath(".//input[not(@id='toggle-"+id+"')]"), function(node){ if(box.checked != node.checked) node.click(); }, cell)//./ancestor::table[@id='selectors'] }); else { addEvent( box, "click", function(e){ for(let m = 0; m < flip.length; m++) { let node = $X(compileXPath("./ancestor::td[1]/..//input[@id='toggle-"+flip[m]+"']"), cell);//./ancestor::table[@id='selectors'] if(node && (box.checked != node.checked)) node.click();//consider not doing this in an event manor. } }); } label.classList.add("flip"); } if(k.event) addEvent( box, "click", k.event); if(k.alt) box.alt = box.title = label.alt = label.title = k.alt; } else if(k.text || k.title) { let span = k.span = document.createElement("span"); //span.type="button"; cell.appendChild(span) span.appendChild(document.createTextNode(k.text || k.title)); //span.value=k.text || k.title; if(k.toggle) { span.className = "toggle"; addEvent( span, "click", function(e){ let t = $X(compileXPath("./ancestor::table[@id='selectors']//input[@id='toggle-"+k.toggle+"'] | ./ancestor::table[@id='selectors']//select[@id='"+k.toggle+"']"), e.target); if(t) { switch(t.nodeName) { case "INPUT": t.click(); break; case "SELECT": span.alt = span.title = "Click to advance the selection, hold shift and click to rewind."; let len = t.options.length; t.selectedIndex = (((e.shiftKey?-1:1) * (e.ctrlKey?2:1)) + len + t.selectedIndex) % len; simulateHTMLEvent(t, "change"); break; } } }); } if(k.event) { span.className = "toggle"; addEvent(span, "click", k.event); } if(k.alt) span.alt = span.title = k.alt; if(typeof(k.text) === "undefined" && typeof(k.title) !== "undefined" && k.title != "") span.classList.add("title"); } else if(typeof(k) === "string") { cell.appendChild(document.createTextNode(k)); } } if(br) cell.appendChild(document.createElement("br")); } return buttons; } function makeFloater(dobj, base){ var obj = dobj.data || {}; var server = dobj.host; // log(obj); var top = base && base.firstChild; if(!top) { top = $X("//table[@class='breadCrumbs']"); if(top)//adjust base base = top.parentNode; } var after = $X("./a[@name='maincontent']", base); // log(base, top, title) GM_addStyle([//{ // ".cheat-sheet { float:right; display:inline; padding-top:0.9em; }", ".cheat-sheet {padding-bottom:0.5em;}",// position:relative; top:-0.5em;}" // ".cheat-sheet tr { display:inline; }", ".cheat-sheet .box { display:inline-block; vertical-align:top; text-align:center; margin-left:2ch; margin-right:-1ch;}", // ".cheat-sheet .box > *:after {content:\",\"}", // ".cheat-sheet .box:after { content: \" \"; }", // ".cheat-sheet tr:after { content: \",\"; }", // ".cheat-sheet tr:last-of-type:after { content: \" \"; }", // ".cheat-sheet td:before { content: \" \"; }", ".cheat-sheet select.providers { width:20ch; }", ".cheat-sheet select.quicksearch, .cheat-sheet select.quicksearch-days { width:18ch; }", // ".cheat-sheet td { padding-left: 0.7ch; }", // ".cheat-sheet td:first-child { text-align:right; }", ".cheat-sheet select.quicksearch form { display:none; }", ".clearBoth { clear:both; }", ].join("\n\n"));//} function nr(a){ let row = document.createElement("div"); row.className = "box" mr.apply(null, [row].concat(Array.slice(arguments))); return row; } function mr(row, a){ if(arguments.length >= 2) { var pre = ""; var post = ""; if(a) { pre = '<a href="'+a+'">'; post = '</a>'; } for(var a = 2; a < arguments.length; a++) nri(row, pre + arguments[a] + post); return row; } } function nri(row, code){ row.appendChild(b = document.createElement("div")); b.innerHTML = code; return row; } let it = document.createElement("div") it.className = "cheat-sheet"; let s = it.appendChild(document.createElement("span")); s.style.cssText = "display:block;"; let tb = it; { let tr = tb.appendChild(document.createElement("div")); tr.className="box"; let select = document.createElement("select"); select.className="providers" let header = select.appendChild(document.createElement("option")) header.appendChild(document.createTextNode("Jump To Provider")) for(var Host of Providers.activeProviders) { if(Host != server){ var pobj = Providers.getProvider(Host); if(pobj != obj && !pobj.stealth) { var option = document.createElement("option"); option.appendChild(document.createTextNode((pobj.data && pobj.data.title) || Host)); option.value = Host; select.appendChild(option); } } } if(select.options.length){ tr.appendChild(select); addEvent(select, "keyup", function(e){ if(e.keyCode === 17) select.setAttribute("ctrl", (false)); }); addEvent(select, "keydown", function(e){ if(e.keyCode === 17 && select.getAttribute("ctrl") != "true") select.setAttribute("ctrl", (true)); }); addEvent(select, "focusout", function(e){ select.removeAttribute("ctrl"); }); addEvent(select, "change", function(e){ if(this.value) if(select.getAttribute("ctrl") == "true") GM_openInTab("http://"+this.value); else window.location="http://"+this.value; }); } mr(tr, "BANGAuthenticate.dll?Action=AuthCheck&URL=MyAccount.htm&ForceLoginFlag=0", "My Account"); if((obj.features && obj.features.wishlist) || (obj.wishlist)) mr(tr, "WishList.htm", "Wish List" + ((obj.wishlist && obj.wishlist.length)?" ("+obj.wishlist.length+")":""));//.cells[0].colSpan = "2"; } var time = function(next){ if(next < 0) next = "Now" else if(next >= (72 * 60 * 60 * 1000)) next = Math.floor(next / (24 * 60 * 60 * 1000)) + " Days"; else if(next >= (48 * 60 * 60 * 1000)) next = Math.floor(next / (24 * 60 * 60 * 100)) / 10 + " Days"; else if(next >= (12 * 60 * 60 * 1000)) next = Math.floor(next / (60 * 60 * 1000)) + " Hours"; else if(next >= (90 * 60 * 1000)) next = Math.floor(next / (60 * 60 * 100)) / 10 + " Hours"; else if(next >= (90 * 1000)) next = Math.floor(next / (60 * 1000)) + " Mins"; else //if(next >= 1000) next = Math.floor(next / 1000) + " Sec"; return next; } if(obj.checkedout){ var next = ""; if(obj.checkedout.length > 0) { next = obj.checkedout.sort(function(a, b){ return a.expires - b.expires; })[0].expires - (new Date().getTime()); if(next < 0) next = "("+time(-next)+")"; else next = time(next); } if(obj.max) tb.appendChild(nr("MyBookshelf.htm", "Items", obj.checkedout.length + " / " + obj.max.items, next)); else tb.appendChild(nr("MyBookshelf.htm", "Items", obj.checkedout.length, next)); } else if(obj.max) tb.appendChild(nr("MyBookshelf.htm", "Items", obj.max.items)); if((obj.features && obj.features.waitinglist) || (obj.waitinglist) || (obj.max && obj.max.holds)) { let maxholds = (obj.waitinglist?obj.waitinglist.length:"?") + " / " + (obj.max && obj.max.holds || "?") let maxdur = obj.max && (obj.max.hold_duration + " Days") || "" if(obj.waitinglist && obj.waitinglist.length) { let next = obj.waitinglist.sort(function(a, b){ return a.estimate - b.estimate; })[0].estimate; if(next < 0) next = time((obj.max.hold_duration * 1000 * 24 * 60 * 60) + next) else next = "("+time(next)+")" tb.appendChild(nr("MyWaitingList.htm", "Holds", maxholds, maxdur, next)) } else tb.appendChild(nr("MyWaitingList.htm", "Holds", maxholds, maxdur)); //if(obj.max.hold_duration) // tb.appendChild(nr("help-policies-borrowing.htm", obj.max.hold_duration, "Days"));//.cells[0].colSpan = "2"; } //TODO make cart smarter!!! tb.appendChild(nr("Cart.htm", "Cart")); //* if(!obj.noSearch) {//Quick Search //tr = document.createElement("tr"); //tr.appendChild(td = document.createElement("td")); // td.colSpan = "2"; //td.appendChild(select = document.createElement("select")); var tr = document.createElement("div") tr.className="box"; let select = tr.appendChild(document.createElement("select")); select.className="quicksearch"; tr.appendChild(document.createElement("br")); let days = tr.appendChild(document.createElement("select")); days.className="quicksearch-days"; { let option = days.appendChild(document.createElement("option")); option.appendChild(document.createTextNode("Ignore Collection Date")) option.value = ""; } for each(var day in DaysOfInterest) { let option = days.appendChild(document.createElement("option")); option.appendChild(document.createTextNode((day == QuickSearchsDays)?day + " days (default)":day + " days")) option.value = day; } days.value = GM_getValue("QuickSearch.Days", QuickSearchsDays); /* All Known search parameters var n = ({ Type:"Advanced", FullTextCriteria:"", FullTextField:["","All","Creator","ISBN"][0],//All=="Title" Title:"", Creator:"", ISBN:"", // Keyword:"", Format:[25,425].join(") OR (LCPU.lftyFormatTypeID = "),//OMG OMG OMG OMG X_X // Availcopies:">*0", Language:"", Publisher:"", Subject:"", Award:"", // UserRating:"", CollDate:">*30", OnSaleDate:">*30", Price:">*30", PerPage:[5,10,25][1], Sort:"SortBy="+["title","author","date","rank","CollDate"][1], }); //*/ var QuickSearchs = {}; QuickSearchs["Advanced Seach"] = "AdvancedSearch.htm"; QuickSearchs["Quick Search"] = 0; //^_^ Hack QuickSearchs["New Titles"] = {}; //* //evilFormatDivider for each(let cls in Formats.Classes) { let formats = obj.formats?cls.formats.filter(function(f){return obj.formats.indexOf(f) > -1;}):cls.formats; if(formats.length > 0) QuickSearchs["New " + cls.titles] = { /*post:{ CollDate: ">*" + QuickSearchsDays, PerPage: QuickSearchsCount, Sort: "SortBy=title", Format: joinFormats(formats), },*/ formats:formats, }; } //*/ QuickSearchs["Rated Titles"] = "BANGSearch.dll?Type=Advanced&UserRating=1&URL=SearchResultsUserRank.htm&PerPage="+QuickSearchsCount; QuickSearchs["Recommend Titles"] = "BANGSearch.dll?Type=Advanced&RecommendedTitles=1&URL=SearchResultsRecommend.htm&PerPage="+QuickSearchsCount; for(let a in QuickSearchs) { let option = document.createElement("option"); option.appendChild(document.createTextNode(a)); select.appendChild(option); let n = QuickSearchs[a]; switch(typeof(n)){ case "object":{ if(n.post) { let form = document.createElement("form"); form.method = "POST"; form.action="BANGSearch.dll?PerPage="+QuickSearchsCount + (n.search?"&" + n.search:""); form.name="QuickSearch-"+a; for(let a in n.post) { let input = document.createElement("input"); input.type="hidden"; input.name=a; input.value=n.post[a]; form.appendChild(input); } option.appendChild(form); } break; } case "string":{ option.value = n; break; } case "number":{ if(!n) select.selectedIndex = select.options.length - 1; break; } } } tb.appendChild(tr); let form = document.createElement("form"); addEvent(select, "keyup", function(e){ if(e.keyCode === 17 && form.target != "_self") form.target = "_self"; }); addEvent(select, "keydown", function(e){ if(e.keyCode === 17 && form.target != "_blank") form.target = "_blank"; }); addEvent(select, "focusout", function(e){ form.target = "_self"; }); addEvent(select, "change", function(e){ var op = this.options[this.options.selectedIndex]; /**/ if(op_form = $X(compileXPath("form"), op)){ op_form.target = form.target; op_form.submit(); } else /**/if(op.hasAttribute("value")){ if(form.target === "_blank") GM_openInTab("http://" + server + "/" + this.value); else window.location="http://" + server + "/" + this.value; } else if((n = QuickSearchs[op.textContent]) && typeof(n) === "object"){ // let form = document.createElement("form"); form.method = "POST"; form.action="BANGSearch.dll?"+(n.search || ""); form.name="QuickSearch-"+a; let post = { PerPage: QuickSearchsCount, Sort: "SortBy=title", }; if(n.formats) post.Format = joinFormats(n.formats); if(days.value) post.CollDate = ">*" + days.value; for(let a in post) { let input = document.createElement("input"); input.type="hidden"; input.name=a; input.value=post[a]; form.appendChild(input); } op.appendChild(form); // if(confirm("Submit:\n" + JSON.stringify(post))) { GM_setValue("QuickSearch.Days", days.value) form.submit(); } //if targeting another tab, the form injected may be captured by the next call, submitting stale data. op.removeChild(form); } }); }//*/ if(after){ insertAfter(it, after); } else if(top){ insertBefore(it, top); if(top.tagName=="BR") top.style.lineHeight="0px"; } if(it.parentNode){//ugly hack ~_~ keeps floaters from screwing up other stuff. Shouldn't have any side effects (we hope). let d = it.parentNode.appendChild(document.createElement("div")); d.className = "clearBoth"; } } //} function evalNoScript(noscript, box, wrap, deAmp) { let code = (noscript && noscript.textContent) || ((noscript==null)?"":noscript); if(deAmp) code = code.replace(/&/g,"&"); if(typeof(wrap) == "string") wrap = [wrap]; else if(!isArray(wrap)) wrap = []; var tag = "div"; if(box && box.innerHTML === "") { box.innerHTML = code; return box; } let out = undefined; let div = document.createElement(tag); let i = wrap.length, j = i; while(i--) code = "<"+wrap[i]+">" + code + "</"+wrap[i]+">"; div.innerHTML = code; while(j--) div = div.firstChild; if(box) { while(div.firstChild) box.appendChild(div.firstChild); out = box; } else { if(noscript.parentNode) { var last = noscript.nextSibling; out = div.childNodes.length; while(div.firstChild) noscript.parentNode.insertBefore(div.firstChild, last); } else{ out = Array.slice(div.childNodes); for each(var r in out) div.removeChild(r); } } return out; } function simulateHTMLEvent(node, type, bubbles, cancelable) { var evt = document.createEvent("HTMLEvents"); evt.initEvent(type, bubbles, cancelable); return node.dispatchEvent(evt); } function scriptReEval(script, newText, docWrite){ if(!newText) newText = script.innerHTML; if(docWrite === true || docWrite === undefined) { docWrite = "rfn"+ Math.ceil(Math.random() * 100000) newText = newText.replace(/document\.write/g, docWrite) } let rscript = document.createElement("script"); if(typeof docWrite === "string" && docWrite !== ""){ //Up to the user let fscript = document.createElement("script"); fscript.type=script.type; fscript.id = "s"+docWrite fscript.innerHTML = "function "+docWrite+"(text){ document.getElementById('"+ fscript.id +"').insertAdjacentHTML('afterend', text); }"; insertAfter(fscript, script) rscript.setAttribute("document.write", docWrite); } let rscript = document.createElement("script"); rscript.type=script.type; rscript.innerHTML = newText insertAfter(rscript, script); return rscript; } //{Xpath function $W(_xpath, node, array, transform){ array = array || []; var res; if(isXPathExpression(_xpath)){ res = _xpath.evaluate(node || document, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); } else{ var doc = (node)?(node.ownerDocument || node):(node = document); try{ res = doc.evaluate(_xpath, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); } catch(e){ log([e].concat(Array.slice(arguments))); return; } } if(transform) { var args = Array.slice(arguments, 4); for (let j, i = 0; i < res.snapshotLength; ++i) if((j = transform.apply(transform, [res.snapshotItem(i), i].concat(args))) !== undefined) array.push(j); } else for (let i = 0; i < res.snapshotLength; ++i) array.push(res.snapshotItem(i)); return array; } function $X(_xpath, node, result){//to search in a frame, you must traverse it's .contentDocument attribute. if(isXPathExpression(_xpath)){ return _xpath.evaluate(node || document, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0); } else{ var doc = (node)?(node.ownerDocument || node):(node = document); try{ return doc.evaluate(_xpath, node, null, result || XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0); } catch(e){ log([e].concat(Array.slice(arguments))); return null; } } } function $Y(_xpath, node, transform){ var res; if(isXPathExpression(_xpath)){ res = _xpath.evaluate(node || document, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); } else{ var doc = (node)?(node.ownerDocument || node):(node = document); try{ res = doc.evaluate(_xpath, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); } catch(e){ log([e].concat(Array.slice(arguments))); return; } } if(transform) { var args = Array.slice(arguments, 3); for (let i = 0, j; i < res.snapshotLength; ++i) if((j = transform.apply(null, [res.snapshotItem(i), i].concat(args))) !== undefined) yield j; } else for (let i = 0; i < res.snapshotLength; ++i) yield res.snapshotItem(i); } function $Z(_xpath, func, node){ var res; if(isXPathExpression(_xpath)){ res = _xpath.evaluate(node || document, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); } else{ var doc = (node)?(node.ownerDocument || node):(node = document); try{ res = doc.evaluate(_xpath, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); } catch(e){ log([e].concat(Array.slice(arguments))); return null; } } var args = Array.prototype.slice.call(arguments, 3); var i = 0; for (; i < res.snapshotLength; ++i) func.apply(null, [res.snapshotItem(i), i].concat(args)); return i; } function compileXPath(xpath, namespaceURLMapper){ let cache = {}; if(typeof(XPathProfilingLevel) !== "undefined" && XPathProfilingLevel > 0) { compileXPath = function(_xpath, _namespaceURLMapper){ if(isXPathExpression(_xpath)) return _xpath;//dipshit, you don't need to compile this! if(compileXPath.CompiledXPath.hasOwnProperty(_xpath)) { let expr = compileXPath.CompiledXPath[_xpath]; if(XPathProfilingLevel > 0) ++expr.lookups; return expr; } let expr; try{ expr = document.createExpression(_xpath, _namespaceURLMapper); } catch(e){ log([e].concat(Array.slice(arguments))); return null; } switch(XPathProfilingLevel){ case 1: if(typeof(expr.evaluate.bind) === "function"){ expr = { __proto__:expr, source:_xpath, lookups:1, evaluate:expr.evaluate.bind(expr), evaluateWithContext:expr.evaluateWithContext.bind(expr), }; break; }//if you don't have bind, fall through to the next level that doesn't require bind. case 2: expr = { __proto__:expr, source:_xpath, lookups: 1, evaluated: 0, evaluate:function(){ this.evaluated++; return this.__proto__.evaluate.apply(this.__proto__, arguments); }, evaluateWithContext: function(){ this.evaluated++; return this.__proto__.evaluateWithContext.apply(this.__proto__, arguments); }, }; break; } return compileXPath.CompiledXPath[_xpath] = expr; } log(cache); //used for later inspection of the cache } else { compileXPath = function(_xpath, _namespaceURLMapper){ if(isXPathExpression(_xpath)) return _xpath;//dipshit, you don't need to compile this! if(compileXPath.CompiledXPath.hasOwnProperty(_xpath)) return compileXPath.CompiledXPath[_xpath]; let expr; try{ expr = document.createExpression(_xpath, _namespaceURLMapper); } catch(e){ log([e].concat(Array.slice(arguments))); return null; } return compileXPath.CompiledXPath[_xpath] = expr; } } compileXPath.CompiledXPath = cache; return compileXPath.apply(this, arguments); } function xPathListContains(name, value, seperator){ if(seperator === undefined) seperator = " "; let sep = SmartQuotes(seperator); return "contains(concat("+sep+", "+name+", "+sep+"), "+SmartQuotes(seperator+value+seperator)+")"; } function isXPathExpression(xpath){ let t = isXPathExpression = function(_xpath){ if(_xpath instanceof XPathExpression || isXPathExpression.XPathExpressionPrototype.isPrototypeOf(_xpath)) return true; //duck type it! let xpp = isXPathExpression.XPathExpressionPrototype; for(let k in xpp) if(typeof _xpath[k] !== typeof xpp[k]) return false; return true; }//note: this will not work if you created your expressions some other way than this way. t.XPathExpressionPrototype = Object.getPrototypeOf(document.createExpression(".", null)); return t.apply(this, arguments); } //}//{HTML Maintanance function insertAsParent(new_parent, target) { return new_parent.appendChild(target.parentNode.replaceChild(new_parent, target)) && new_parent; } function insertAfter(insert, after){ return after.parentNode.insertBefore(insert, after.nextSibling); } function insertBefore(insert, before){ if(before && before.parentNode) return before.parentNode.insertBefore(insert, before); log("insertBefore", arguments);} function replace(n, old){ if(n !== old) old.parentNode.replaceChild(n, old); return n;} function remove(r){return r.parentNode.removeChild(r);} function getTag(element){ let expr = compileXPath("ancestor-or-self::*[1]"); return (getTag = function(elm){ return $X(expr, elm); })(element); } //} function addEvent( obj, type, fn, capture ) { obj.addEventListener( type, fn, capture?capture:false ); } function removeEvent( obj, type, fn, capture ) { obj.removeEventListener( type, fn, capture?capture:false ); } function log() { var arg; switch(arguments.length) { case 1: arg = arguments[0]; break; case 0: arg = null; break; default: arg = Array.slice(arguments); break; } // if(JSON && JSON.stringify) // arg = JSON.stringify(arg); /**/if(typeof(unsafeWindow.console) != "undefined" && typeof(unsafeWindow.console.log) === "function") unsafeWindow.console.log(arg); /**/else/**/ if(typeof(GM_log) === "function") GM_log(arg); return arg; } //}