Wikidata Episode Generator

Creates QuickStatements for Wikidata episode items from Wikipedia episode lists

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Wikidata Episode Generator
// @version      0.13.2
// @description  Creates QuickStatements for Wikidata episode items from Wikipedia episode lists
// @author       CennoxX
// @namespace    https://greasyfork.org/users/21515
// @homepage     https://github.com/CennoxX/userscripts
// @supportURL   https://github.com/CennoxX/userscripts/issues/new?title=[Wikipedia%20Episode%20Generator]%20
// @match        https://en.wikipedia.org/wiki/*
// @connect      www.wikidata.org
// @connect      www.imdb.com
// @connect      www.fernsehserien.de
// @icon         https://www.wikidata.org/static/favicon/wikidata.ico
// @grant        GM.xmlHttpRequest
// @grant        GM.setClipboard
// @grant        GM.registerMenuCommand
// @grant        unsafeWindow
// @license      MIT
// ==/UserScript==
/* jshint esversion: 11 */
/* eslint no-prototype-builtins: "off" */
/* eslint no-return-assign: "off" */
/* eslint curly: "off" */
/* globals mw */

(function() {
    "use strict";
    unsafeWindow.generateEpisodes = generateEpisodes;
    GM.registerMenuCommand("Generate episode items for Wikidata", generateEpisodes, "w");
    async function generateEpisodes(){
        console.clear();
        var article = mw.config.get("wgTitle");
        var response = await fetch(`/w/api.php?action=query&prop=revisions|pageprops&titles=${encodeURIComponent(article)}&rvslots=*&rvprop=content|ids&formatversion=2&format=json`);
        var data = await response.json();
        var version = Object.values(data.query.pages)[0].revisions[0];
        var articleText = version.slots.main.content;
        var subArticles = articleText.match(/{{:(.*)}}/g);
        if (subArticles != null){
            console.log("Loading sub articles from Wikipedia…");
            var sub = subArticles.map(i => encodeURIComponent(i.replace(/{{:|}}/g,""))).join("|");
            response = await fetch(`/w/api.php?action=query&prop=revisions|pageprops&titles=${sub}&rvslots=*&rvprop=content|ids&formatversion=2&format=json`);
            var subData = await response.json();
            var subtexts = Object.values(subData.query.pages);
            subtexts.forEach(i => {articleText = articleText.replace(i.title, i.revisions[0].slots.main.content)});
        }
        var wikibaseId = Object.values(data.query.pages)[0].pageprops?.wikibase_item;
        if (wikibaseId == null){
            console.error("The item for this Wikipedia article doesn't exist at Wikidata, please create it first.");
            return;
        }
        response = await GM.xmlHttpRequest({
            method: "GET",
            url: `https://www.wikidata.org/w/api.php?action=wbgetentities&props=claims&sitefilter=dewiki&ids=${wikibaseId}&format=json`,
            onload: function(response) {
                return response;
            }
        });
        var jsonObj = Object.values((JSON.parse(response.responseText)).entities)[0];
        var seriesId = 0;
        window.addEventListener("unhandledrejection", async (ex) => {
            var propertyUndefined = ex.reason.message.match(/.*\.(P\d+) is undefined/);
            if (propertyUndefined){
                var response = await GM.xmlHttpRequest({
                    method: "GET",
                    url: `https://www.wikidata.org/w/api.php?action=wbgetentities&props=labels&ids=${propertyUndefined[1]}&languages=en&format=json`,
                    onload: function(response) {
                        return response;
                    }
                });
                var url = `https://www.wikidata.org/wiki/${seriesId}`;
                console.error(`The property "${JSON.parse(response.responseText).entities[propertyUndefined[1]].labels.en.value}" (${propertyUndefined[1]}) is missing from the series item (${url})`);
            }
        });
        if (article.split("List of ").length == 2){
            seriesId = jsonObj.claims.P360[0].qualifiers.P179[0].datavalue.value.id;
        }else if(article.split("season").length == 2){
            seriesId = jsonObj.claims.P179[0].mainsnak.datavalue.value.id;
        }else{
            seriesId = wikibaseId;
        }
        response = await GM.xmlHttpRequest({
            method: "GET",
            url: `https://www.wikidata.org/w/api.php?action=wbgetentities&props=sitelinks|claims|labels&sitefilter=dewiki&ids=${seriesId}&format=json`,
            onload: function(response) {
                return response;
            }
        });
        jsonObj = Object.values((JSON.parse(response.responseText)).entities)[0];
        var series = jsonObj.labels.de.value;
        var seriesEn = jsonObj.labels.en.value;
        var networkId = jsonObj.claims.P449[0].mainsnak.datavalue.value.id;
        var imdbId = (jsonObj.claims.hasOwnProperty("P345")) ? jsonObj.claims.P345[0].mainsnak.datavalue.value : prompt("IMDb-Id");
        var fsId = (jsonObj.claims.hasOwnProperty("P5327")) ? jsonObj.claims.P5327[0].mainsnak.datavalue.value : prompt("Fernsehserien.de-Id");
        var originalLanguageId = jsonObj.claims.P364[0].mainsnak.datavalue.value.id;
        var originalCountryId = jsonObj.claims.P495[0].mainsnak.datavalue.value.id;
        var seasons = jsonObj.claims.P527?.sort((a,b) => a.qualifiers.P1545[0].datavalue.value - b.qualifiers.P1545[0].datavalue.value).map(i => i.mainsnak.datavalue.value.id) ?? console.error("season items are missing");
        if (location.pathname.includes("season")){
            var epSeason = document.title.match(/season (\d+)\)/)[1];
            seasons = jsonObj.claims.P527.filter(i => i.qualifiers.P1545[0].datavalue.value == Number(epSeason)).map(i => i.mainsnak.datavalue.value.id);
        }
        var wikilinks = [];
        var plainlinks = [];
        var eps = articleText.split(/{{(?:#invoke:)?Episode list.*\n|==+ *Season (\d+) |== ?(Special)s?(?: episodes?|: [^=]+)? ?(?:\((?:19|20)\d\d(?:[-–](?:19|20)?\d\d)?\))? ?==/i).filter(i => i).map(i => i.split(/\n}}\n/)[0]).slice(1);
        if (articleText.match(/== *Season \d+ /i)){
            var tempSeason = 0;
            eps.forEach((e,i) => {
                if (Number(e) || e.toLowerCase() == "special"){
                    tempSeason = e;
                    eps.splice(i + 1,1);
                }else{
                    eps[i] += "\n |Season = " + tempSeason;
                }
            });
            eps = eps.filter(i => !Number(i) && i.toLowerCase() != "special" && !i.match(/Season *= *Special *$/i));
        }
        eps = eps.flatMap(episode => {
            var lines = episode.split("\n");
            var numParts = lines.find(line => line.startsWith("| NumParts"))?.split("=")[1]?.trim();
            if (numParts) {
                var newEpisodes = [];
                for (let i = 1; i <= parseInt(numParts); i++) {
                    var firstNumberLine = true;
                    var firstNumberLine_2 = true;
                    var newEpisode = lines.map(line => {
                        if (line.startsWith("| EpisodeNumber_") && firstNumberLine) {
                            firstNumberLine = false;
                            return "| EpisodeNumber = " + lines.find(l => l.startsWith("| EpisodeNumber_" + i)).split("=")[1].trim();
                        } else if (line.startsWith("| EpisodeNumber2") && firstNumberLine_2) {
                            firstNumberLine_2 = false;
                            return "| EpisodeNumber2 = " + lines.find(l => l.startsWith("| EpisodeNumber2_" + i)).split("=")[1].trim();
                        } else if (line.startsWith("| Title")) {
                            return "| Title = " + line.split("=")[1].trim() + ", part " + i;
                        } else if (!line.startsWith("| EpisodeNumber_") && !line.startsWith("| EpisodeNumber2_") && !line.startsWith("| NumParts")) {
                            return line;
                        }
                    }).filter(Boolean).join("\n");
                    newEpisodes.push(newEpisode);
                }
                return newEpisodes;
            } else {
                return episode;
            }
        });
        var episodes = eps.map(i => {
            wikilinks = wikilinks.concat([...i.matchAll(/\[\[(.*?)\]\]/g)].map(i => i[1]));
            plainlinks = plainlinks.concat([...i.matchAll(/\[\[(.*?)\]\]/g)].map(i => i[1].split("|")[1] ?? i[1]));
            if (i.match("DirectedBy_?1?2? *= *(.+) *(?:\n|\\|)") == null){console.error("DirectedBy\n",i)}
            if (i.match("WrittenBy_?1?2? *= *(.+) *(?:\n|\\|)") == null){console.error("WrittenBy\n",i)}
            return {
                "NR_GES": (i.match("EpisodeNumber *= *(\\d+) *(?:\n|\\|)") ?? ["",(console.error("EpisodeNumber\n",i),prompt("EpisodeNumber\n" + i.match("EpisodeNumber.*\n")) ?? 0)])[1],
                "NR_ST": (i.match("EpisodeNumber2 *= *(\\d+) *(?:\n|\\|)") ?? i.match("EpisodeNumber *= *(\\d+) *(?:\n|\\|)") ?? ["",(console.error("EpisodeNumber2\n",i),prompt("EpisodeNumber2\n" + i.match("EpisodeNumber2.*\n")) ?? 0)])[1],
                "OT": (i.match("Title *= *(.+) *(?:\n|\\|)") ?? ["",(console.error("Title\n",i),prompt("Title\n" + i.match("Title.*\n")))])[1].replace(/<!--.*?-->/i,"").split(/\[\[.*\||\[\[|\]\]/g).join("").trim().replace(/{{'-}}/g,"'").replace(/{{visible anchor\|(.*)}}/g,"$1").replace(/^(.*?)(?=[^\dXVI]{2}.) ?[,:\-–]? \(?(?:part )?([\dXVI]+)\)?/i,"$1, part $2").replace(/{{va\|(?:.*\|)?(.*)}}/, "$1"),
                "SL": (i.match("Title *= *\\[\\[(.+)\\]\\] *(?:\n|\\|)") ?? ["",""])[1].split("|")[0],
                "EA": getDate((i.match("OriginalAirDate *= *(.+) *(?:\n|\\|)") ?? ["",(console.error("OriginalAirDate\n",i),"")])[1]),
                "REG": [...new Set([...[...(i.matchAll("DirectedBy_?1?2? *= *(.+) *(?:\n|\\|)"))].map(i => i[1]).join(" ").matchAll(new RegExp(plainlinks.join("|"),"g"))].map(i => i[0]).filter(i => i != "").map(p => wikilinks.filter(w => (w.split("|")[1] ?? w) == p)[0]?.split("|")[0]))].filter(i => i),
                "DRB": [...new Set([...[...(i.matchAll("WrittenBy_?1?2? *= *(.+) *(?:\n|\\|)"))].map(i => i[1]).join(" ").matchAll(new RegExp(plainlinks.join("|"),"g"))].map(i => i[0]).filter(i => i != "").map(p => wikilinks.filter(w => (w.split("|")[1] ?? w) == p)[0]?.split("|")[0]))].filter(i => i),
                "PROD": (i.match("ProdCode *= *(.*) *(?:\n|\\|)") ?? ["",""])[1].split("<ref")[0],
                "ST": (i.match(/Season *= *(\d+) *$/) ?? ["",""])[1],
            };
        });
        var seasonId = 0;
        var episodeId = 0;
        var articleName = location.pathname.replace(/^\/wiki\//,"");
        var source = `S143	Q328	S4656	"https://en.wikipedia.org/w/index.php?title=${articleName}&oldid=${version.revid}"`;
        var output = "";
        var lastEA = "";
        var lastEp = "";
        var seasonMissing = false;
        episodes.forEach(i => {
            if (i.ST){
                seasonId = Number(i.ST) - 1;
            }else if (Number(i.NR_ST) < episodeId){
                seasonId++;
            }
            i.season = seasonId;
            episodeId = i.NR_ST;
            seasonMissing = (seasonId != -1 && seasons && !seasons[i.season]);
        });

        await GetEpisodeItems(seriesId, episodes, originalCountryId);

        if (!seasons || seasonMissing){
            var quickstatements = await GetSeasonItems(seriesId);
            if (quickstatements != ""){
                //write Statements for adding seasons to series
                console.warn("Please rerun Wikidata Episode Generator after running these QuickStatements.");
                output += quickstatements;
            }else{
                //write CREATE-Statements for seasons
                console.warn("Please wait some time after running these QuickStatements and then rerun Wikidata Episode Generator.");
                episodeId = 0;
                episodes.forEach(ep => {
                    if (!seasons || !seasons[ep.season - 1]) {
                        //new season, first episode
                        if (Number(ep.NR_ST) < episodeId){
                            output += `LAST	P582	+${lastEA}T00:00:00Z/11	P291	${originalCountryId}	${source}
LAST	P1113	${episodeId}	${source}
`;
                        }
                    }
                    if (!seasons || !seasons[ep.season]) {
                        //new season
                        if (ep.NR_GES == 1 || Number(ep.NR_ST) < episodeId){
                            output += `CREATE
LAST	Len	"${seriesEn}, season ${ep.season + 1}"
LAST	Lde	"${series}/Staffel ${ep.season + 1}"
LAST	Den	"season of ${seriesEn}"
LAST	Dde	"Staffel von ${series}"
LAST	P31	Q3464665
LAST	P179	${seriesId}	P1545	"${ep.season + 1}"	${source}
LAST	P364	${originalLanguageId}	${source}
LAST	P495	${originalCountryId}	${source}
LAST	P449	${networkId}	${source}
LAST	P580	+${ep.EA}T00:00:00Z/11	P291	${originalCountryId}	${source}
`;
                        }
                        //last episode
                        if (ep.NR_GES == episodes.length){
                            output += `LAST	P582	+${ep.EA}T00:00:00Z/11	P291	${originalCountryId}	${source}
LAST	P1113	${ep.NR_ST}	${source}
`;
                        }
                    }
                    lastEA = ep.EA;
                    ep.season = seasonId;
                    episodeId = ep.NR_ST;
                });
            }
        }
        else
        {
            if (fsId){
                await GetFSData(fsId, episodes);
            }
            if (imdbId){
                await GetIMDbIds(imdbId, episodes);
            }
            await GetWikipediaLinks(episodes);
            //write CREATE-Statements for episodes, get DRB and REG
            episodes.forEach(ep => {
                var epText = `CREATE\n`;
                if (ep.hasOwnProperty("OT") && !ep.hasOwnProperty("Len"))
                    epText += `LAST	Len	"${ep.OT}"\n`;
                if (ep.hasOwnProperty("DT") && !ep.hasOwnProperty("Lde"))
                    epText += `LAST	Lde	"${ep.DT}"\n`;
                if (ep.SL != "" && !ep.hasOwnProperty("Senwiki"))
                    epText += `LAST	Senwiki	"${ep.SL}"\n`;
                if (!ep.hasOwnProperty("Den"))
                    epText += `LAST	Den	"episode of ${seriesEn}"\n`;
                if (!ep.hasOwnProperty("Dde"))
                    epText += `LAST	Dde	"Folge von ${series}"\n`;
                if (!ep.hasOwnProperty("P179P1545"))
                    epText += `LAST	P179	${seriesId}	P1545	"${ep.NR_GES}"	${source}\n`;
                if (!ep.hasOwnProperty("P31"))
                    epText += `LAST	P31	Q21191270\n`;
                if (!ep.hasOwnProperty("P1476"))
                    epText += `LAST	P1476	en:"${ep.OT}"	${source}\n`;
                if (!ep.hasOwnProperty("P4908P1545")){
                    if (seasons[ep.season]){
                        epText += `LAST	P4908	${seasons[ep.season]}	P1545	"${ep.NR_ST}"	${source}\n`;
                    } else {
                        console.error("season not found\n", ep);
                    }
                }
                if (!ep.hasOwnProperty("P577P291O"))
                    epText += `LAST	P577	+${ep.EA}T00:00:00Z/11	P291	${originalCountryId}	${source}\n`;
                if (!ep.hasOwnProperty("P449"))
                    epText += `LAST	P449	${networkId}	${source}\n`;
                if (!ep.hasOwnProperty("P364"))
                    epText += `LAST	P364	${originalLanguageId}	${source}\n`;
                if (!ep.hasOwnProperty("P495"))
                    epText += `LAST	P495	${originalCountryId}	${source}\n`;
                if (ep.hasOwnProperty("EAD") && ep.EAD != "" && !ep.hasOwnProperty("P577P291")){
                    ep.EAD = ep.EAD.replace(/(\d{2})\.(\d{2})\.(\d{4})/,"$3-$2-$1");
                    var fsSource = `S248	Q54871129	S5327	"${fsId}"	S813	+${new Date().toISOString().slice(0,10)}T00:00:00Z/11`;
                    epText += `LAST	P577	+${ep.EAD}T00:00:00Z/11	P291	Q183	${fsSource}\n`;
                }
                ep.REGid.forEach(reg => {
                    if (!ep.hasOwnProperty("P57") || !(new RegExp(ep.P57)).test(reg))
                        epText += `LAST	P57	${reg}	${source}\n`;
                });
                ep.DRBid.forEach(drb => {
                    if (!ep.hasOwnProperty("P58") || !(new RegExp(ep.P58)).test(drb))
                        epText += `LAST	P58	${drb}	${source}\n`;
                });
                if (ep.PROD != "" && !ep.hasOwnProperty("P2364"))
                    epText += `LAST	P2364	"${ep.PROD}"	${source}\n`;
                if (ep.hasOwnProperty("imdb") && !ep.hasOwnProperty("P345"))
                    epText += `LAST	P345	"${ep.imdb}"\n`;
                if (ep.hasOwnProperty("qid")){
                    epText = epText.replace(/(CREATE\n)?LAST/g, ep.qid).replace(/CREATE\n/g, "");
                    if (lastEp.hasOwnProperty("qid")){
                        if (!lastEp.hasOwnProperty("P156") && !lastEp.hasOwnProperty("P179P156"))
                            epText = `${lastEp.qid}	P156	${ep.qid}	${source}\n${epText}`;
                        if (!ep.hasOwnProperty("P155") && !ep.hasOwnProperty("P179P155"))
                            epText += `${ep.qid}	P155	${lastEp.qid}	${source}\n`;
                    }
                }
                lastEp = ep;
                if (epText.match(/\bundefined\b/))
                    console.error("episode includes undefined value\n", epText);
                output += epText;
            });
        }
        if (!jsonObj.claims.hasOwnProperty("P5327") && fsId)
            output += `${seriesId}	P5327	"${fsId}"
`;
        if (output)
            console.log(output);
        console.warn(output ? "Please check all QuickStatements for correctness before execution at https://quickstatements.toolforge.org/#/batch." : "No missing QuickStatements found.");
        GM.setClipboard(output);
    }
    function getDate(episodeDate){
        var result = episodeDate.replace(/{{start date ?(?:\|[md]f=y(?:es)?)?\|(\d+) ?\|(\d+) ?\|(\d+) ?(?:\|[md]f=y(?:es)?)?}}.*/i,"$1-$2-$3").replace(/-(\d)\b/g,"-0$1");
        if (!/[1-2][09]\d\d-[0-1]\d-[0-3]\d/.test(result)){
            console.error("OriginalAirDate",episodeDate);
        }
        return result;
    }
    function compareString(title){
        if (!title)
            return null;
        var partEp = title.match(/^(.*?)(?=[^\d]{2}.) ?[,:\-–]? \(?(?:(?:part|teil) )?#?([\dXVI]+|one|two)\)? *$/i);
        if (partEp)
        {
            var part2 = partEp[2].replace(/one/i, "1").replace(/two/i, "2");
            title = partEp[1] + (Number(part2) ? getRomanNumber(part2) : part2);
        }
        return title.trim().toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/&/i, "and").replace(/^the |^a |[\u200B-\u200D\uFEFF]| |\.|'|’|\(|\)|:|,|‚|\?|!|„|“|"|‘|…|\.|—|–|-/gi,"");
    }
    function getRomanNumber(n){
        return n = Number(n) && "x10ix9v5iv4i1".replace(/(\D+)(\d+)/g,(_,r,d)=>r.repeat(n / d,n %= d));
    }
    function levenshteinDistance(str1, str2){
        if (!str1 || !str2){
            return 100;
        }
        var track = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null));
        for (let i = 0; i <= str1.length; i += 1){
            track[0][i] = i;
        }
        for (let j = 0; j <= str2.length; j += 1){
            track[j][0] = j;
        }
        for (let j = 1; j <= str2.length; j += 1){
            for (let i = 1; i <= str1.length; i += 1){
                var indicator = str1[i - 1] === str2[j - 1] ? 0 : 1;
                track[j][i] = Math.min(
                    track[j][i - 1] + 1,
                    track[j - 1][i] + 1,
                    track[j - 1][i - 1] + indicator,
                );
            }
        }
        return track[str2.length][str1.length];
    }
    function SetEpisodeAttributes(output, episode){
        output.P31 = "Q21191270";
        output.P179 = "series";
        for (const prop in episode) {
            output[prop] = episode[prop].value;
        }
        return output;
    }
    async function GetSparqlResponse(request){
        var resp = await fetch("https://query-main.wikidata.org/sparql?format=json", {
            "headers": {
                "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
                "Accept": "application/sparql-results+json"
            },
            "body": "query=" + encodeURIComponent(request).replaceAll("%20","+").replaceAll("%5Cd","%5C%5Cd"),
            "method": "POST",
            "mode": "cors"
        });
        return await resp.json();
    }
    async function GetEpisodeItems(qid, episodes, originalCountryId){
        console.log("Loading episode items from Wikidata…");
        var request = `SELECT ?qid ?seasonNr ?Len ?Lde ?Den ?Dde ?Dnl ?Senwiki (GROUP_CONCAT(DISTINCT REPLACE(STR(?P57all), ".*/", ""); SEPARATOR = "|") AS ?P57) (GROUP_CONCAT(DISTINCT REPLACE(STR(?P58all), ".*/", ""); SEPARATOR = "|") AS ?P58) (MIN(?P155all) AS ?P155) (MIN(?P156all) AS ?P156) ?P179P155 ?P179P156 ?P179P1545 ?P345 ?P364 ?P449 ?P495 ?P577P291 ?P577P291O ?P1476 ?P2364 ?P4908P1545 WHERE {
  BIND(wd:${qid} AS ?series)
  ?pSeries ps:P179 ?series.
  ?q wdt:P31 wd:Q21191270;
    p:P179 ?pSeries.
  OPTIONAL { ?pSeries pq:P1545 ?P179P1545. }
  OPTIONAL {
    ?q p:P4908 _:0.
    _:0 pq:P1545 ?P4908P1545.
  }
  OPTIONAL {
    ?q wdt:P4908 _:1.
    _:1 rdfs:label ?wSeason.
    FILTER((LANG(?wSeason)) = "en")
    BIND(REPLACE(STR(?wSeason), ".* ", "") AS ?seasonNr)
  }
  OPTIONAL {
    ?q rdfs:label ?Len.
    FILTER((LANG(?Len)) = "en")
  }
  OPTIONAL {
    ?q rdfs:label ?Lde.
    FILTER((LANG(?Lde)) = "de")
  }
  OPTIONAL {
    ?q schema:description ?Den.
    FILTER((LANG(?Den)) = "en")
  }
  OPTIONAL {
    ?q schema:description ?Dde.
    FILTER((LANG(?Dde)) = "de")
  }
  OPTIONAL {
    ?q schema:description ?Dnl.
    FILTER((LANG(?Dnl)) = "nl")
  }
  OPTIONAL {
	?q p:P577 _:2.
	_:2 ps:P577 ?P577P291;
  	pq:P291 wd:Q183.
  }
  OPTIONAL {
	?q p:P577 _:3.
	_:3 ps:P577 ?P577P291O;
  	pq:P291 wd:${originalCountryId}.
  }
  OPTIONAL {
    ?Senwiki schema:about ?q;
      schema:isPartOf <https://en.wikipedia.org/>;
      schema:name _:4.
  }
  OPTIONAL { ?pSeries pq:P155 ?P179P155. }
  OPTIONAL { ?pSeries pq:P156 ?P179P156. }
  OPTIONAL { ?q wdt:P57 ?P57all. }
  OPTIONAL { ?q wdt:P58 ?P58all. }
  OPTIONAL { ?q wdt:P345 ?P345. }
  OPTIONAL { ?q wdt:P364 ?P364. }
  OPTIONAL { ?q wdt:P449 ?P449. }
  OPTIONAL { ?q wdt:P495 ?P495. }
  OPTIONAL { ?q wdt:P1476 ?P1476. }
  OPTIONAL { ?q wdt:P2364 ?P2364. }
  OPTIONAL { ?q wdt:P155 ?P155all. }
  OPTIONAL { ?q wdt:P156 ?P156all. }
  BIND(REPLACE(STR(?q),".*/","") as ?qid).
  FILTER NOT EXISTS {?qid wdt:P1 wd:Q${new Date() % 1000}.}
}
GROUP BY ?qid ?seasonNr ?Len ?Lde ?Den ?Dde ?Dnl ?Senwiki ?P179P155 ?P179P156 ?P179P1545 ?P345 ?P364 ?P449 ?P495 ?P577P291 ?P577P291O ?P1476 ?P2364 ?P4908P1545`;
        var resp = await GetSparqlResponse(request);
        var sparqlEps = resp.results.bindings;
        for (let ep of sparqlEps){
            var sparqlEp = episodes.filter(e => compareString(e.OT) == compareString(ep.Len.value));
            if (sparqlEp.length == 1){
                sparqlEp[0] = SetEpisodeAttributes(sparqlEp[0], ep);
            }else{
                var matchedEp = episodes.reduce(function(prev, curr) {
                    return levenshteinDistance(compareString(prev.OT), compareString(ep.Len.value)) <= levenshteinDistance(compareString(curr.OT), compareString(ep.Len.value)) ? prev : curr;
                });
                var epSeason;
                if (location.pathname.includes("season")){
                    epSeason = document.title.match(/season (\d+)\)/)[1];
                }else{
                    epSeason = matchedEp.season + 1;
                }
                var matchedEpNr = epSeason + "x" + (matchedEp.NR_ST.length == 1 ? "0" : "") + matchedEp.NR_ST;
                var epNr = (ep.seasonNr?.value ?? "0") + "x" + ((ep.P4908P1545?.value.length ?? 1) == 1 ? "0" : "") + (ep.P4908P1545?.value ?? "0");
                if (matchedEp.NR_GES != ep.P179P1545.value && epNr != matchedEpNr){
                    var message = `Wikipedia: #${matchedEp.NR_GES} / ${matchedEpNr} ${matchedEp.OT}
Wikidata-ID from Wikidata: #${ep.P179P1545.value} / ${epNr} ${ep.Len.value}`;
                    if (confirm("fuzzy match?\n" + message)){
                        matchedEp = SetEpisodeAttributes(matchedEp, ep);
                        message = "matched:\n" + message;
                    }else{
                        message = "not matched:\n" + message;
                    }
                    console.log(message);
                }else{
                    matchedEp = SetEpisodeAttributes(matchedEp, ep);
                }
            }
        }
    }
    async function GetSeasonItems(qid){
        console.log("Loading season items from Wikidata…");
        var request = `SELECT ?quickstatements WHERE {
  BIND(wd:${qid} AS ?series)
  ?qid wdt:P31 wd:Q3464665;
    p:P179 _:s.
  _:s ps:P179 ?series;
    pq:P1545 ?number.
  BIND(CONCAT(REPLACE(STR(?series), ".*/", ""), "	P527	", REPLACE(STR(?qid), ".*/", ""), "	P1545	\\"", ?number, "\\"") AS ?quickstatements)
  MINUS {
    ?series p:P527 ?pQid.
    ?pQid ps:P527 ?qid.
    ?pQid pq:P1545 ?number.
  }
  FILTER NOT EXISTS {?qid wdt:P1 wd:Q${new Date() % 1000}.}
}
ORDER BY (xsd:integer(?number))`;
        var resp = await GetSparqlResponse(request);
        var sparqlSeasons = resp.results.bindings;
        return sparqlSeasons.map(i => i.quickstatements.value).join("\n");
    }
    async function GetFSData(fsId, episodes){
        var url = `https://www.fernsehserien.de/${fsId}/episodenguide`;
        console.log(`loading German labels and dates from Fernsehserien.de… (${url})`);
        var fsDatas = [];
        var response = await GM.xmlHttpRequest({
            method: "GET",
            url: url,
            onload: function(response) {
                return response;
            }
        });
        var parser = new DOMParser();
        var xmlDoc = parser.parseFromString(response.responseText,"text/html");
        fsDatas = [...xmlDoc.querySelectorAll("a[data-event-category=liste-episoden]")].map(a => {return{"Lde": a.querySelector("div:nth-child(7)>span").innerText.replace(/^(.*?)(?=[^\d]{2}.) ?[,:\-–]? \(?(?:Teil )?(\d+)\)?/i,"$1 – Teil $2"), "Len": a.querySelector("div:nth-child(7)>span.episodenliste-schmal")?.innerText.replace(/ \(a\.k\.a\..*?\)/i,""), "nr": a.querySelector("div:nth-child(2)")?.firstChild?.nodeValue, "epNr": a.querySelector("span:nth-child(1)").innerText.replace(".","x"), "ead": a.querySelector("div:nth-child(8)").childNodes[0]?.data?.trim()}});
        if (fsDatas.length == 0)
            return;
        for (var ep of episodes){
            let ot = ep.OT;
            var fsData = fsDatas.filter(id => compareString(id.Len) == compareString(ot));
            if (fsData.length == 1){
                if (fsData[0].Lde != "–"){
                    ep.DT = fsData[0].Lde;
                    if (fsData[0].ead != "")
                        ep.EAD = fsData[0].ead;
                }
            }else{
                var matchedEp = fsDatas.reduce(function(prev, curr) {
                    return levenshteinDistance(compareString(prev.Len), compareString(ot)) < levenshteinDistance(compareString(curr.Len), compareString(ot)) ? prev : curr;
                });
                var epSeason;
                if (location.href.includes("season")){
                    epSeason = document.title.match(/season (\d+)\)/)[1];
                }else{
                    epSeason = ep.season + 1;
                }
                var epNr = epSeason + "x" + (ep.NR_ST.length == 1 ? "0" : "") + ep.NR_ST;
                if (matchedEp.Lde != "–"){
                    if (!((ep.NR_GES != matchedEp.nr || ep.NR_GES != matchedEp.nr - 1 || ep.NR_GES != matchedEp.nr + 1)
                        && (epSeason != matchedEp.epNr.split("x")[0] || epSeason + 1 != matchedEp.epNr.split("x")[0] || epSeason - 1 != matchedEp.epNr.split("x")[0])
                        && (ep.NR_ST != matchedEp.epNr.split("x")[1] || ep.NR_ST + 1 != matchedEp.epNr.split("x")[1] || ep.NR_ST - 1 != matchedEp.epNr.split("x")[1])))
                    {
                        var message = `Wikipedia: #${ep.NR_GES} / ${epNr} ${ot}
data from Fernsehserien.de: #${matchedEp?.nr ?? 0} / ${matchedEp.epNr} ${matchedEp.Len}`;
                        if (confirm("fuzzy match?\n" + message)){
                            ep.DT = matchedEp.Lde;
                            if (matchedEp.ead != "")
                                ep.EAD = matchedEp.ead;
                            message = "matched:\n" + message;
                        }else{
                            message = "not matched:\n" + message;
                        }
                        console.log(message);
                    }else{
                        ep.DT = matchedEp.Lde;
                        if (matchedEp.ead != "")
                            ep.EAD = matchedEp.ead;
                    }
                }
            }
        }
    }
    async function GetIMDbIds(imdbId, episodes){
        var url = `https://www.imdb.com/search/title/?series=${imdbId}&view=simple&sort=release_date,asc&count=250`;
        console.log(`loading IDs from IMDb… (${url})`);
        var imdbIds = [];
        var startEp = 1;
        var allEps = 0;
        do{
            var response = await GM.xmlHttpRequest({
                method: "GET",
                url: `${url}&start=${startEp}`,
                onload: function(response) {
                    return response;
                }
            });
            var parser = new DOMParser();
            var xmlDoc = parser.parseFromString(response.responseText,"text/html");
            imdbIds = imdbIds.concat([...xmlDoc.querySelectorAll(".ipc-metadata-list-summary-item .dli-ep-title")].map(i => {return {"title": i.innerText, "id": i.firstChild.href.match(/tt\d+/)?.[0], "nr": i.parentElement.parentElement.querySelector(".dli-title").innerText.split(".")[0]}}));
            allEps = 250;
            startEp = startEp + 250;
        } while (imdbIds.length < allEps);
        for (var ep of episodes){
            let ot = ep.OT;
            let imdbId = imdbIds.filter(id => compareString(id.title) == compareString(ot));
            if (imdbId.length == 1){
                ep.imdb = imdbId[0].id;
            }else{
                var matchedEp = imdbIds.reduce(function(prev, curr) {
                    return levenshteinDistance(compareString(prev.title), compareString(ot)) <= levenshteinDistance(compareString(curr.title), compareString(ot)) ? prev : curr;
                });
                if (ep.NR_GES == matchedEp.nr || ep.NR_GES == Number(matchedEp.nr) + 1 || ep.NR_GES == Number(matchedEp.nr) - 1){
                    ep.imdb = matchedEp.id;
                }else{
                    var epSeason;
                    if (location.pathname.includes("season")){
                        epSeason = document.title.match(/season (\d+)\)/)[1];
                    }else{
                        epSeason = ep.season + 1;
                    }
                    var message = `Wikipedia: #${ep.NR_GES} / ${epSeason}x${(ep.NR_ST.length == 1 ? "0" : "")}${ep.NR_ST} ${ot}
IMDb-ID from IMDb: #${matchedEp.nr} ${matchedEp.title}`;
                    if (confirm("fuzzy match?\n" + message)){
                        ep.imdb = matchedEp.id;
                        message = "matched:\n" + message;
                    }else{
                        message = "not matched:\n" + message;
                    }
                    console.log(message);
                }
            }
        }
    }
    async function GetWikipediaLinks(episodes){
        console.log("Loading Wikidata items from Wikipedia links…");
        var cachedLinks = [];
        var cachedRequests = episodes.flatMap((i) => [
            ...i.DRB,
            ...i.REG,
            ...(i.SL ? [i.SL] : []),
        ]).map(i => encodeURIComponent(i)).join("|");
        var parts = cachedRequests.match(/(?:[^|]+\|){0,49}[^|]+/g);
        if (parts){
            for (var part of parts){
                var cachedRequestsResponse = await fetch(`/w/api.php?action=query&prop=pageprops&ppprop=wikibase_item&redirects=1&titles=${part}&format=json`);
                var { query: { redirects, pages } } = await cachedRequestsResponse.json();
                cachedLinks.push(...Object.values(pages).map(p => ({
                    name: redirects?.find(({ from }) => from === p.title)?.to || p.title,
                    qid: p.pageprops?.wikibase_item,
                })));
            }
        }
        cachedLinks = [...new Set(cachedLinks)];
        for (var ep of episodes){
            ep.DRBid = [];
            ep.REGid = [];
            for (let drb of ep.DRB){
                var cachedDRB = cachedLinks.find(i => i.name == drb);
                if (cachedDRB && cachedDRB.qid){
                    ep.DRBid.push(cachedDRB.qid);
                }
            }
            for (let reg of ep.REG){
                var cachedREG = cachedLinks.find(i => i.name == reg);
                if (cachedREG && cachedREG.qid){
                    ep.REGid.push(cachedREG.qid);
                }
            }
            if (ep.SL != ""){
                let epSL = ep.SL;
                var cachedSL = cachedLinks.find(i => i.name == epSL);
                if (cachedSL && cachedSL.qid){
                    ep.qid = cachedSL.qid;
                }
            }
        }
    }
})();