Zum Inhalt springen

„Benutzer:Mps/AnimeListenUpdater.cs“ – Versionsunterschied

aus Wikipedia, der freien Enzyklopädie
Inhalt gelöscht Inhalt hinzugefügt
KKeine Bearbeitungszusammenfassung
K lgpassword und lgtoken als POST statt Queryparameter; Refactoring
Zeile 8: Zeile 8:
using System.Net;
using System.Net;
using System.Reflection;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text;
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
Zeile 15: Zeile 16:
namespace AnimeListenUpdater
namespace AnimeListenUpdater
{
{

#region Exceptions
#region Exceptions

class BusinessException : Exception
internal class BusinessException : Exception
{
{
public BusinessException(string message)
public BusinessException(string message)
: base(message)
: base(message)
{ }
{
}
}
}


class MediawikiException : BusinessException
internal class MediawikiException : BusinessException
{
{
public string Code { get; }

public MediawikiException(string message)
public MediawikiException(string message)
: base(message)
: base(message)
{ }
{
}


public MediawikiException(string code, string info)
public MediawikiException(string code, string info)
: base("Mediawiki error \"" + code + ": " + info + "\"")
: base("Mediawiki error \"" + code + ": " + info + "\"")
{
{
Code = code;
Code = code;
}
}

public string Code { get; }
}
}


class MediawikiLagException : MediawikiException
internal class MediawikiLagException : MediawikiException
{
{
public int LagTime { get; private set; }

public MediawikiLagException(string code, string info)
public MediawikiLagException(string code, string info)
: base("Mediawiki error \"" + code + ": " + info + "\"")
: base("Mediawiki error \"" + code + ": " + info + "\"")
{
{
LagTime = GetLagTime(info);
LagTime = GetLagTime(info);
}
}

public int LagTime { get; private set; }


public static int GetLagTime(string info)
public static int GetLagTime(string info)
{
{
int lagtime = -1;
var lagtime = -1;
Match match = Regex.Match(info, "Waiting for [^ ]*: ([0-9.-]+) seconds lagged");
var match = Regex.Match(info, "Waiting for [^ ]*: ([0-9.-]+) seconds lagged");
if (match.Success) int.TryParse(match.Groups[1].Value, out lagtime);
if (match.Success)
{
int.TryParse(match.Groups[1].Value, out lagtime);
}
return lagtime;
return lagtime;
}
}
}
}

#endregion
#endregion


public class Mediawiki
public static class Mediawiki
{
{
#region Netzwerk- und Mediawiki-Funktionen
#region Netzwerk- und Mediawiki-Funktionen
static CookieContainer cookieContainer = new CookieContainer();
static readonly string userAgentString = GetUserAgentString();


private static readonly CookieContainer cookieContainer = new CookieContainer();
static string GetUserAgentString()
private static readonly string userAgentString = GetUserAgentString();

private static string GetUserAgentString()
{
{
AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
var assemblyName = Assembly.GetExecutingAssembly().GetName();


// Programmname und -version
// Programmname und -version
string userAgent = assemblyName.Name + "/" + assemblyName.Version.Major + "." + assemblyName.Version.Minor;
var userAgent = assemblyName.Name + "/" + assemblyName.Version.Major + "." + assemblyName.Version.Minor;
// Umgebungsinformationen (OS, Laufzeitumgebung)
// Umgebungsinformationen (OS, Laufzeitumgebung)
userAgent += " (" + Environment.OSVersion.VersionString + "; .NET CLR " + Environment.Version + ")";
userAgent += " (" + Environment.OSVersion.VersionString + "; .NET CLR " + Environment.Version + ")";
Zeile 78: Zeile 88:
private static string EncodeQuery(string[,] query)
private static string EncodeQuery(string[,] query)
{
{
string result = "";
var result = "";
for (int i = 0; i < query.GetLength(0); i++)
for (var i = 0; i < query.GetLength(0); i++)
{
{
result += "&" + query[i, 0];
result += "&" + query[i, 0];
string value = query[i, 1];
var value = query[i, 1];
if (value != null) result += "=" + Uri.EscapeDataString(value);
if (value != null)
{
result += "=" + Uri.EscapeDataString(value);
}
}
}
return result;
return result;
Zeile 90: Zeile 103:
private static string MultipartQuery(string[,] postData, string boundaryIdentifier)
private static string MultipartQuery(string[,] postData, string boundaryIdentifier)
{
{
StringBuilder sb = new StringBuilder();
var sb = new StringBuilder();
for (int i = 0; i < postData.GetLength(0); i++)
for (var i = 0; i < postData.GetLength(0); i++)
{
{
sb.Append("--");
sb.Append("--");
Zeile 104: Zeile 117:
}
}


static HttpWebRequest GenerateHttpWebRequest(string language, string[,] query)
private static HttpWebRequest GenerateHttpWebRequest(string language, string[,] query)
{
{
string url = "https://";
var url = "https://";
if (language == "wikidata") url += "www.wikidata.org"; else url += language + ".wikipedia.org";
if (language == "wikidata")
{
url += "www.wikidata.org";
}
else
{
url += language + ".wikipedia.org";
}
url += "/w/api.php?format=xml";
url += "/w/api.php?format=xml";
url += EncodeQuery(query);
url += EncodeQuery(query);


HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
var request = (HttpWebRequest)WebRequest.Create(url);


request.UserAgent = userAgentString;
request.UserAgent = userAgentString;
Zeile 122: Zeile 142:
public static HttpWebResponse HttpGet(string language, string[,] query)
public static HttpWebResponse HttpGet(string language, string[,] query)
{
{
HttpWebRequest request = GenerateHttpWebRequest(language, query);
var request = GenerateHttpWebRequest(language, query);
return (HttpWebResponse)request.GetResponse();
return (HttpWebResponse)request.GetResponse();
}
}
Zeile 128: Zeile 148:
public static HttpWebResponse HttpPost(string language, string[,] query, string[,] postData)
public static HttpWebResponse HttpPost(string language, string[,] query, string[,] postData)
{
{
HttpWebRequest request = GenerateHttpWebRequest(language, query);
var request = GenerateHttpWebRequest(language, query);
request.Method = "POST";
request.Method = "POST";
if (postData != null)
if (postData != null)
{
{
string boundary = Guid.NewGuid().ToString();
var boundary = Guid.NewGuid().ToString();
byte[] postContent = new UTF8Encoding(false).GetBytes(MultipartQuery(postData, boundary));
var postContent = new UTF8Encoding(false).GetBytes(MultipartQuery(postData, boundary));
request.ContentType = "multipart/form-data; boundary=" + boundary;
request.ContentType = "multipart/form-data; boundary=" + boundary;
using (Stream stream = request.GetRequestStream())
using (var stream = request.GetRequestStream())
{
{
stream.Write(postContent, 0, postContent.Length);
stream.Write(postContent, 0, postContent.Length);
Zeile 146: Zeile 166:
{
{
using (response)
using (response)
using (Stream stream = response.GetResponseStream())
using (XmlReader reader = XmlReader.Create(stream))
{
{
using (var stream = response.GetResponseStream())
if (!reader.ReadToFollowing("api")) throw new MediawikiException("Malformed response");

while (reader.Read())
{
{
CheckForError(reader);
using (var reader = XmlReader.Create(stream))
onXmlNode(reader);
{
if (!reader.ReadToFollowing("api"))
{
throw new MediawikiException("Malformed response");
}

while (reader.Read())
{
CheckForError(reader);
onXmlNode(reader);
}
}
}
}
}
}
Zeile 161: Zeile 188:
public static string CalcMd5Hash(byte[] data)
public static string CalcMd5Hash(byte[] data)
{
{
System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
MD5 md5 = new MD5CryptoServiceProvider();
byte[] md5Hash = md5.ComputeHash(data);
var md5Hash = md5.ComputeHash(data);
return BitConverter.ToString(md5Hash).Replace("-", "").ToLowerInvariant();
return BitConverter.ToString(md5Hash).Replace("-", "").ToLowerInvariant();
}
}
Zeile 170: Zeile 197:
if (reader.NodeType == XmlNodeType.Element)
if (reader.NodeType == XmlNodeType.Element)
{
{
if (reader.LocalName == "warnings") Debug.WriteLine(" warning: " + reader.ReadInnerXml());
if (reader.LocalName == "warnings")
{
Debug.WriteLine(" warning: " + reader.ReadInnerXml());
}
else if (reader.LocalName == "error")
else if (reader.LocalName == "error")
{
{
string code = reader.GetAttribute("code");
var code = reader.GetAttribute("code");
string info = reader.GetAttribute("info");
var info = reader.GetAttribute("info");
Debug.WriteLine(" error \"" + code + "\": " + info);
Debug.WriteLine(" error \"" + code + "\": " + info);
if (code == "maxlag")
if (code == "maxlag")
{
throw new MediawikiLagException(code, info);
throw new MediawikiLagException(code, info);
else
}
throw new MediawikiException(code, info);
throw new MediawikiException(code, info);
}
}
}
}
}
}


public enum TokenType { Csrf, Watch, Patrol, Rollback, UserRights, Login, CreateAccount }
public enum TokenType
{
Csrf,
Watch,
Patrol,
Rollback,
UserRights,
Login,
CreateAccount
}


public static string GetToken(string lang, TokenType tokenType)
public static string GetToken(string lang, TokenType tokenType)
{
{
string tokenName = tokenType.ToString().ToLowerInvariant();
var tokenName = tokenType.ToString().ToLowerInvariant();


string[,] paramList = new[,] {
string[,] paramList =
{ "action", "query" },
{
{ "action", "query" },
{ "meta", "tokens" },
{ "meta", "tokens" },
{ "type", tokenName }
{ "type", tokenName }
};
};


string token = null;
string token = null;
Zeile 209: Zeile 250:
public static void Login(string lang, NetworkCredential credentials)
public static void Login(string lang, NetworkCredential credentials)
{
{
string[,] paramList = new[,]
string[,] post =
{
{
{ "action", "login" },
{ "lgpassword", credentials.Password },
{ "lgname", credentials.UserName },
{ "lgtoken", GetToken(lang, TokenType.Login) }
};
{ "lgpassword", credentials.Password },
string[,] paramList =
{ "lgtoken", GetToken(lang, TokenType.Login) }
};
{
{ "action", "login" },
{ "lgname", credentials.UserName }
};


TraverseHttpWebResponse(HttpPost(lang, paramList, null), reader =>
TraverseHttpWebResponse(HttpPost(lang, paramList, post), reader =>
{
{
if ((reader.NodeType == XmlNodeType.Element) && (reader.LocalName == "login"))
if ((reader.NodeType == XmlNodeType.Element) && (reader.LocalName == "login"))
{
{
string result = reader.GetAttribute("result");
var result = reader.GetAttribute("result");
if (result != "Success")
if (result != "Success")
{
{
if (result == "Throttled") result += $" (Please wait {reader.GetAttribute("wait")}s)";
if (result == "Throttled")
{
result += $" (Please wait {reader.GetAttribute("wait")}s)";
}
throw new MediawikiException(result);
throw new MediawikiException(result);
}
}
Zeile 233: Zeile 280:
public static void Logout(string lang)
public static void Logout(string lang)
{
{
using (HttpWebResponse response = HttpGet(lang, new[,] { { "action", "logout" } })) { }
using (HttpGet(lang, new[,] { { "action", "logout" } })) { }
}
}


Zeile 241: Zeile 288:
public class Program
public class Program
{
{
static Mediawiki wiki;
private static string editToken;
static string editToken = null;


private static Dictionary<string, string> ParseTemplate(string template)
public class AnimeItem
{
{
public string List;
var result = new Dictionary<string, string>();
public int Level;
public int Year;
public string Type;
public string EpisodeCount;
public string Title;
public string Link;
public string JapaneseTitle;
public string JapaneseTranscription;
public List<string> AlternativeTitles;
public List<string> Studios;


var startPos = template.IndexOf("{{", StringComparison.Ordinal);
public static AnimeItem Parse(Dictionary<string, string> parsedTemplate, string list)
var lastPos = template.LastIndexOf("}}", StringComparison.Ordinal);
if ((startPos >= 0) && (lastPos >= 0))
{
{
var split = template.Substring(startPos + 2, lastPos - startPos - 2).Split('|');
AnimeItem result = new AnimeItem();
var parameters = new List<string>();
try

// Wikilinks wieder zusammenführen, d.h. "[[A", "B]]" zu "[[A|B]]".
var doubleBracketCount = 0;
for (var i = 0; i < split.Length; i++)
{
{
result.List = list;
if (doubleBracketCount == 0)
result.Level = int.Parse(parsedTemplate["1"]);
result.Year = int.Parse(parsedTemplate["2"]);
result.Type = parsedTemplate["3"].Trim();
result.EpisodeCount = parsedTemplate["4"].Trim();
result.Title = parsedTemplate["5"].Trim();
if (parsedTemplate.TryGetValue("link", out result.Link)) result.Link = result.Link.Trim();
if (parsedTemplate.ContainsKey("j")) result.JapaneseTitle = parsedTemplate["j"].Trim(); else result.JapaneseTitle = "";
if (parsedTemplate.ContainsKey("jt")) result.JapaneseTranscription = parsedTemplate["jt"].Trim(); else result.JapaneseTranscription = "";
result.AlternativeTitles = new List<string>();
result.Studios = new List<string>();
for (int i = 1; i <= 10; i++)
{
{
string paramName = "a" + i;
parameters.Add(split[i]);
}
if (parsedTemplate.ContainsKey(paramName)) result.AlternativeTitles.Add(parsedTemplate[paramName].Trim());
paramName = "s" + i;
else
{
if (parsedTemplate.ContainsKey(paramName)) result.Studios.Add(parsedTemplate[paramName].Trim());
parameters[parameters.Count - 1] += "|" + split[i];
}
}
return result;
}
catch (KeyNotFoundException) { return null; }
catch (FormatException) { return null; }
}

public string GetWikilink()
{
string result = "[[";
if (!string.IsNullOrEmpty(Link)) result += Link + "|";
result += Title + "]]";
return result;
}

public static string GetWikiArticle(string wikilink)
{
if (string.IsNullOrEmpty(wikilink)) return "";
else
{
// entferne Wikilinkklammern und splitte an der ersten Pipe
string link = wikilink.TrimStart('[').TrimEnd(']').Split(new char[] { '|' }, 2)[0];
// erstes Element (Zielartikel) mit Großbuchstaben beginnen lassen und zurückgeben
link = Char.ToUpperInvariant(link[0]) + link.Substring(1);
return link;
}
}

public string GetYearListEntry()
{
StringBuilder sb = new StringBuilder();
sb.Append("{{Anime-Listeneintrag|");
sb.Append(0);
sb.Append("|");
sb.Append(Year);
sb.Append("|");
sb.Append(Type);
sb.Append("|");
sb.Append(EpisodeCount);
sb.Append("|");
sb.Append(Title);
if (!string.IsNullOrEmpty(Link))
{
sb.Append("|link=");
sb.Append(Link);
}
if (!string.IsNullOrEmpty(JapaneseTitle))
{
sb.Append("|j=");
sb.Append(JapaneseTitle);
}
if (!string.IsNullOrEmpty(JapaneseTranscription))
{
sb.Append("|jt=");
sb.Append(JapaneseTranscription);
}
for (int aIdx = 0; aIdx < AlternativeTitles.Count; aIdx++)
{
sb.Append("|a");
sb.Append(aIdx + 1);
sb.Append("=");
sb.Append(AlternativeTitles[aIdx]);
}
for (int sIdx = 0; sIdx < Studios.Count; sIdx++)
{
sb.Append("|s");
sb.Append(sIdx + 1);
sb.Append("=");
sb.Append(Studios[sIdx]);
}
sb.Append("|mark=year}}");
return sb.ToString();
}


var curIdx = 0;
public override string ToString()
{
return Title;
}
}

#region Konfiguration
static UnicodeCategory[] validCharTypes = new UnicodeCategory[] {
UnicodeCategory.UppercaseLetter, UnicodeCategory.LowercaseLetter, UnicodeCategory.TitlecaseLetter, UnicodeCategory.ModifierLetter, UnicodeCategory.OtherLetter,
UnicodeCategory.DecimalDigitNumber, UnicodeCategory.LetterNumber, UnicodeCategory.OtherNumber, UnicodeCategory.SpaceSeparator };

static string[] alphaList = new string[] { "0–9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" };
static List<int> yearList = new List<int> { 1900, 1945, 1946, 1962, 1963, 1969, 1970, 1979, 1980, 1984, 1985, 1989, 1990, 1994, 1995, 1997, 1998, 1999 }; // ab 2000 wird automatisch generiert
static Dictionary<string, string> animeTypes = new Dictionary<string, string> {
{ "F", "Film" },
{ "S", "Fernsehserie" },
{ "W", "Web-Anime" },
{ "O", "OVA" },
{ "SP", "Special" },
{ "K", "Kurzfilm" }
};
#endregion

static Dictionary<string, string> ParseTemplate(string template)
{
Dictionary<string, string> result = new Dictionary<string, string>();

int startPos = template.IndexOf("{{");
int lastPos = template.LastIndexOf("}}");
if (startPos >= 0 && lastPos >= 0)
{
string[] split = template.Substring(startPos + 2, lastPos - startPos - 2).Split('|');
List<string> parameters = new List<string>();

// Wikilinks wieder zusammenführen, d.h. "[[A", "B]]" zu "[[A|B]]".
int doubleBracketCount = 0;
for (int i = 0; i < split.Length; i++)
{
if (doubleBracketCount == 0) parameters.Add(split[i]); else parameters[parameters.Count - 1] += "|" + split[i];

int curIdx = 0;
while (curIdx >= 0)
while (curIdx >= 0)
{
{
curIdx = split[i].IndexOf("[[", curIdx);
curIdx = split[i].IndexOf("[[", curIdx, StringComparison.Ordinal);
if (curIdx >= 0)
if (curIdx >= 0)
{
{
Zeile 407: Zeile 327:
while (curIdx >= 0)
while (curIdx >= 0)
{
{
curIdx = split[i].IndexOf("]]", curIdx);
curIdx = split[i].IndexOf("]]", curIdx, StringComparison.Ordinal);
if (curIdx >= 0)
if (curIdx >= 0)
{
{
Zeile 416: Zeile 336:
}
}


for (int paramIdx = 0; paramIdx < parameters.Count; paramIdx++)
for (var paramIdx = 0; paramIdx < parameters.Count; paramIdx++)
{
{
string[] keyValue = parameters[paramIdx].Split(new char[] { '=' }, 2);
var keyValue = parameters[paramIdx].Split(new[] { '=' }, 2);
try
try
{
{
if (keyValue.Length == 2) result.Add(keyValue[0], keyValue[1]); else result.Add(paramIdx.ToString(), parameters[paramIdx]);
if (keyValue.Length == 2)
{
result.Add(keyValue[0], keyValue[1]);
}
else
{
result.Add(paramIdx.ToString(), parameters[paramIdx]);
}
}
}
catch (ArgumentException)
catch (ArgumentException)
Zeile 433: Zeile 360:


// zur Sortierung alle Zeichen außer Buchstaben, Zahlen und Leerzeichen entfernen und Buchstaben in Kleinschreibung umwandeln
// zur Sortierung alle Zeichen außer Buchstaben, Zahlen und Leerzeichen entfernen und Buchstaben in Kleinschreibung umwandeln
static string RemoveDiacriticsAndSpecialChars(string text)
private static string RemoveDiacriticsAndSpecialChars(string text)
{
{
if (text.StartsWith("The ") || text.StartsWith("Der ") || text.StartsWith("Die ") || text.StartsWith("Das ")) text = text.Substring(4);
if (text.StartsWith("The ") || text.StartsWith("Der ") || text.StartsWith("Die ") || text.StartsWith("Das "))
{
text = text.Substring(4);
}


return string.Concat(text.Normalize(NormalizationForm.FormKD).Where(ch => validCharTypes.Contains(CharUnicodeInfo.GetUnicodeCategory(ch)))).Normalize(NormalizationForm.FormKC).ToLowerInvariant();
return string.Concat(text.Normalize(NormalizationForm.FormKD).Where(ch => validCharTypes.Contains(CharUnicodeInfo.GetUnicodeCategory(ch)))).Normalize(NormalizationForm.FormKC).ToLowerInvariant();
}
}


static void WriteContent(string pagetitle, string content, string basetimestamp = null)
private static void WriteContent(string pagetitle, string content, string basetimestamp = null)
{
{
if (editToken == null) editToken = Mediawiki.GetToken("de", Mediawiki.TokenType.Csrf);
if (editToken == null)
{
editToken = Mediawiki.GetToken("de", Mediawiki.TokenType.Csrf);
}


bool badToken;
bool badToken;
Zeile 451: Zeile 384:
{
{
Console.Write("Schreibe " + pagetitle);
Console.Write("Schreibe " + pagetitle);
string md5Hash = Mediawiki.CalcMd5Hash(new UTF8Encoding(false).GetBytes(content));
var md5Hash = Mediawiki.CalcMd5Hash(new UTF8Encoding(false).GetBytes(content));


var post = new string[3 + (basetimestamp != null ? 1 : 0), 2];
var post = new string[3 + (basetimestamp != null ? 1 : 0), 2];
Zeile 466: Zeile 399:
}
}


using (HttpWebResponse response = Mediawiki.HttpPost("de", new[,] {
using (var response = Mediawiki.HttpPost("de", new[,]
{ "assert", "user" },
{
{ "assert", "user" },
{ "action", "edit" },
{ "action", "edit" },
{ "title", pagetitle },
{ "title", pagetitle },
{ "notminor", null },
{ "notminor", null },
{ "summary", "Listen-Aktualisierung per [[Benutzer:Mps/AnimeListenUpdater.cs]]" } },
{ "summary", "Listen-Aktualisierung per [[Benutzer:Mps/AnimeListenUpdater.cs]]" }
},
post))
post))
using (Stream stream = response.GetResponseStream())
using (XmlReader reader = XmlReader.Create(stream))
{
{
using (var stream = response.GetResponseStream())
while (reader.Read()) Mediawiki.CheckForError(reader);
{
using (var reader = XmlReader.Create(stream))
{
while (reader.Read())
{
Mediawiki.CheckForError(reader);
}
}
}
}
}
Console.WriteLine(".");
Console.WriteLine(".");
Zeile 493: Zeile 435:
}
}


static bool BuildAnimeYearList(List<AnimeItem> animeList, int startYear, int endYear, TextWriter writer)
private static bool BuildAnimeYearList(List<AnimeItem> animeList, int startYear, int endYear, TextWriter writer)
{
{
writer.WriteLine("__ABSCHNITTE_NICHT_BEARBEITEN__ __KEIN_INHALTSVERZEICHNIS__");
writer.WriteLine("__ABSCHNITTE_NICHT_BEARBEITEN__ __KEIN_INHALTSVERZEICHNIS__");
Zeile 505: Zeile 447:
writer.WriteLine();
writer.WriteLine();


bool hasAtLeastOneYear = false;
var hasAtLeastOneYear = false;
for (int year = startYear; year <= endYear; year++)
for (var year = startYear; year <= endYear; year++)
{
{
var yearClosure = year;
IEnumerable<AnimeItem> animesInYear = from anime in animeList
IEnumerable<AnimeItem> animesInYear = from anime in animeList
where anime.Year == year
where anime.Year == yearClosure
orderby RemoveDiacriticsAndSpecialChars(anime.Title)
orderby RemoveDiacriticsAndSpecialChars(anime.Title)
select anime;
select anime;


if (animesInYear.Any())
if (animesInYear.Any())
Zeile 518: Zeile 461:
writer.WriteLine("== " + year + " ==");
writer.WriteLine("== " + year + " ==");
writer.WriteLine("{{Anime-Listenkopf|");
writer.WriteLine("{{Anime-Listenkopf|");
foreach (AnimeItem anime in animesInYear)
foreach (var anime in animesInYear)
{
{
writer.WriteLine(anime.GetYearListEntry());
writer.WriteLine(anime.GetYearListEntry());
Zeile 531: Zeile 474:
}
}


static bool BuildAnimeStudioList(List<AnimeItem> animeList, TextWriter writer)
private static bool BuildAnimeStudioList(List<AnimeItem> animeList, TextWriter writer)
{
{
IEnumerable<string> linkedStudios = animeList.SelectMany(anime => anime.Studios) // Studiolisten zusammenführen
IEnumerable<string> linkedStudios = animeList.SelectMany(anime => anime.Studios) // Studiolisten zusammenführen
.Where(studio => studio.StartsWith("[[")) // nach verlinkten Studios filtern
.Where(studio => studio.StartsWith("[[")) // nach verlinkten Studios filtern
.Select(studio => AnimeItem.GetWikiArticle(studio)) // Linkklammern entfernen, nach Pipe separieren und nur erstes Element (Linkziel) zurückgeben
.Select(studio => AnimeItem.GetWikiArticle(studio)) // Linkklammern entfernen, nach Pipe separieren und nur erstes Element (Linkziel) zurückgeben
.Distinct() // doppelte Einträge entfernen
.Distinct() // doppelte Einträge entfernen
.OrderBy(name => RemoveDiacriticsAndSpecialChars(name)); // sortieren
.OrderBy(name => RemoveDiacriticsAndSpecialChars(name)); // sortieren


foreach (string curStudio in linkedStudios)
foreach (var curStudio in linkedStudios)
{
{
writer.WriteLine("{{#ifeq: {{{1}}}|" + curStudio + "|");
writer.WriteLine("{{#ifeq: {{{1}}}|" + curStudio + "|");


Dictionary<string, List<AnimeItem>> animesOfStudio = animeList
var animesOfStudio = animeList
.Where(anime => anime.Studios.Any(studio => AnimeItem.GetWikiArticle(studio) == curStudio)) // nach Anime die vom aktuellen Studio gefertigt wurden filtern
.Where(anime => anime.Studios.Any(studio => AnimeItem.GetWikiArticle(studio) == curStudio)) // nach Anime die vom aktuellen Studio gefertigt wurden filtern
.OrderBy(anime => anime.Year + RemoveDiacriticsAndSpecialChars(anime.Title)) // nach Jahr und Titel sortieren
.OrderBy(anime => anime.Year + RemoveDiacriticsAndSpecialChars(anime.Title)) // nach Jahr und Titel sortieren
.GroupBy(anime => anime.Type) // nach Typ gruppieren
.GroupBy(anime => anime.Type) // nach Typ gruppieren
.ToDictionary(group => group.Key, group => group.ToList()); // in assoziative Liste überführen
.ToDictionary(group => group.Key, group => group.ToList()); // in assoziative Liste überführen


// Generierungsregeln:
// Generierungsregeln:
// ab 10 Einträge pro Typ: eigene Gruppe (Reihenfolge: Fernsehserien, Filme, Original Video Animations, Specials, Kurzfilme, Weitere Anime-Produktionen)
// ab 10 Einträge pro Typ: eigene Gruppe (Reihenfolge: Fernsehserien, Filme, Original Video Animations, Specials, Kurzfilme, Weitere Anime-Produktionen)
// ab 10 Einträge pro Gruppe: zwei Spalten, wobei bei ungerader Anzahl die erste Spalte länger ist
// ab 10 Einträge pro Gruppe: zwei Spalten, wobei bei ungerader Anzahl die erste Spalte länger ist
bool hasCaptions = false;
var hasCaptions = false;
foreach (string animeType in animeTypes.Keys)
foreach (var animeType in animeTypes.Keys)
{
{
#region wenn ein Studio mehr als 10 Animes eines Typs produziert hat, diese separat aufführen
#region wenn ein Studio mehr als 10 Animes eines Typs produziert hat, diese separat aufführen

if (animesOfStudio.ContainsKey(animeType) && animesOfStudio[animeType].Count >= 10)
if (animesOfStudio.ContainsKey(animeType) && (animesOfStudio[animeType].Count >= 10))
{
{
hasCaptions = true;
hasCaptions = true;
Zeile 581: Zeile 525:
}
}
writer.WriteLine("<div style=\"{{Spaltenbreite}}\">");
writer.WriteLine("<div style=\"{{Spaltenbreite}}\">");
foreach (AnimeItem anime in animesOfStudio[animeType])
foreach (var anime in animesOfStudio[animeType])
{
{
writer.WriteLine("* " + anime.Year + ": ''" + anime.GetWikilink() + "''");
writer.WriteLine("* " + anime.Year + ": ''" + anime.GetWikilink() + "''");
Zeile 589: Zeile 533:
animesOfStudio.Remove(animeType);
animesOfStudio.Remove(animeType);
}
}

#endregion
#endregion
}
}


IEnumerable<AnimeItem> otherAnimes = animesOfStudio
IEnumerable<AnimeItem> otherAnimes = animesOfStudio
.SelectMany(kvp => kvp.Value)
.SelectMany(kvp => kvp.Value)
.OrderBy(anime => anime.Year + RemoveDiacriticsAndSpecialChars(anime.Title));
.OrderBy(anime => anime.Year + RemoveDiacriticsAndSpecialChars(anime.Title));


if (otherAnimes.Any())
if (otherAnimes.Any())
{
{
if (hasCaptions) writer.WriteLine("=== Weitere Anime-Produktionen ===");
if (hasCaptions)
{
writer.WriteLine("=== Weitere Anime-Produktionen ===");
}
writer.WriteLine("<div style=\"{{Spaltenbreite}}\">");
writer.WriteLine("<div style=\"{{Spaltenbreite}}\">");
foreach (AnimeItem anime in otherAnimes)
foreach (var anime in otherAnimes)
{
{
string desc;
string desc;
if (animeTypes.ContainsKey(anime.Type)) desc = " (" + animeTypes[anime.Type] + ")"; else desc = "";
if (animeTypes.ContainsKey(anime.Type))
{
desc = " (" + animeTypes[anime.Type] + ")";
}
else
{
desc = "";
}
writer.WriteLine("* " + anime.Year + ": ''" + anime.GetWikilink() + "''" + desc);
writer.WriteLine("* " + anime.Year + ": ''" + anime.GetWikilink() + "''" + desc);
}
}
Zeile 617: Zeile 572:
}
}


static bool BuildMaintainenceInfo(List<AnimeItem> animeList, TextWriter writer)
private static bool BuildMaintainenceInfo(List<AnimeItem> animeList, TextWriter writer)
{
{
var allStudios = animeList.SelectMany(anime => anime.Studios, (anime, studio) => new { Name = studio, List = anime.List });
var allStudios = animeList.SelectMany(anime => anime.Studios, (anime, studio) => new { Name = studio, anime.List }).ToList();


var linkedStudios = allStudios
var linkedStudios = allStudios
.Where(studio => studio.Name.StartsWith("[["))
.Where(studio => studio.Name.StartsWith("[["))
.Select(studio => new { Name = AnimeItem.GetWikiArticle(studio.Name), List = studio.List })
.Select(studio => new { Name = AnimeItem.GetWikiArticle(studio.Name), studio.List })
.GroupBy(studio => studio.Name)
.GroupBy(studio => studio.Name)
.Select(studio => new { Name = studio.Key, Count = studio.Count(), Lists = studio.Select(list => list.List).Distinct().OrderBy(list => list) })
.Select(studio => new { Name = studio.Key, Count = studio.Count(), Lists = studio.Select(list => list.List).Distinct().OrderBy(list => list) })
.OrderBy(studio => studio.Name);
.OrderBy(studio => studio.Name);


var unlinkedStudios = allStudios
var unlinkedStudios = allStudios
.Where(studio => !studio.Name.StartsWith("[["))
.Where(studio => !studio.Name.StartsWith("[["))
.Select(studio => new { Name = studio.Name, List = studio.List })
.Select(studio => new { studio.Name, studio.List })
.GroupBy(studio => studio.Name)
.GroupBy(studio => studio.Name)
.Select(studio => new { Name = studio.Key, Count = studio.Count(), Lists = studio.Select(list => list.List).Distinct().OrderBy(list => list) });
.Select(studio => new { Name = studio.Key, Count = studio.Count(), Lists = studio.Select(list => list.List).Distinct().OrderBy(list => list) })
.ToList();


writer.WriteLine("== Nur teilweise verlinkte Studios ==");
writer.WriteLine("== Nur teilweise verlinkte Studios ==");
Zeile 653: Zeile 609:
foreach (var unlinkedStudio in unlinkedStudios)
foreach (var unlinkedStudio in unlinkedStudios)
{
{
if (!string.IsNullOrEmpty(unlinkedStudio.Name) && unlinkedStudio.Count > 5 && linkedStudios.All(linkedStudio => linkedStudio.Name != unlinkedStudio.Name))
if (!string.IsNullOrEmpty(unlinkedStudio.Name) && (unlinkedStudio.Count > 5) && linkedStudios.All(linkedStudio => linkedStudio.Name != unlinkedStudio.Name))
{
{
writer.WriteLine("|-\n| {0} || {1} || {2}", unlinkedStudio.Name, unlinkedStudio.Count, string.Join(", ", unlinkedStudio.Lists.Select(list => "[[Liste der Anime-Titel/" + list + "|" + list + "]]")));
writer.WriteLine("|-\n| {0} || {1} || {2}", unlinkedStudio.Name, unlinkedStudio.Count, string.Join(", ", unlinkedStudio.Lists.Select(list => "[[Liste der Anime-Titel/" + list + "|" + list + "]]")));
Zeile 660: Zeile 616:
writer.WriteLine("|}\n");
writer.WriteLine("|}\n");


Dictionary<string, List<string>> combinedStudioList = allStudios
var combinedStudioList = allStudios
.Where(studio => !string.IsNullOrEmpty(studio.Name))
.Where(studio => !string.IsNullOrEmpty(studio.Name))
.Select(studio => new { Name = AnimeItem.GetWikiArticle(studio.Name), List = studio.List })
.Select(studio => new { Name = AnimeItem.GetWikiArticle(studio.Name), studio.List })
.GroupBy(studio => studio.Name)
.GroupBy(studio => studio.Name)
.OrderBy(studio => studio.Key)
.OrderBy(studio => studio.Key)
.ToDictionary(group => group.Key, group => group.Select(studio => studio.List).Distinct().ToList());
.ToDictionary(group => group.Key, group => group.Select(studio => studio.List).Distinct().ToList());


writer.WriteLine("== Ähnliche Studionamen ==");
writer.WriteLine("== Ähnliche Studionamen ==");
writer.WriteLine("{| class=\"wikitable sortable\"");
writer.WriteLine("{| class=\"wikitable sortable\"");
writer.WriteLine("! Studio || Ähnliche Namen || Listen");
writer.WriteLine("! Studio || Ähnliche Namen || Listen");
string[] allStudiosArray = combinedStudioList.Keys.ToArray();
var allStudiosArray = combinedStudioList.Keys.ToArray();
for (int i = 0; i < allStudiosArray.Length; i++)
for (var i = 0; i < allStudiosArray.Length; i++)
{
{
string targetStudio = allStudiosArray[i];
var targetStudio = allStudiosArray[i];
Dictionary<string, int> similarStudios = new Dictionary<string, int>();
var similarStudios = new Dictionary<string, int>();
for (int j = 0/*i + 1*/; j < allStudiosArray.Length; j++)
for (var j = 0 /*i + 1*/; j < allStudiosArray.Length; j++)
{
{
if (i == j) continue;
if (i == j)
{
continue;
}


string otherStudio = allStudiosArray[j];
var otherStudio = allStudiosArray[j];


int dist = LevenshteinDistance(RemoveDiacriticsAndSpecialChars(targetStudio), RemoveDiacriticsAndSpecialChars(otherStudio));
var dist = LevenshteinDistance(RemoveDiacriticsAndSpecialChars(targetStudio), RemoveDiacriticsAndSpecialChars(otherStudio));
if (dist <= 2)
if (dist <= 2)
{
{
Zeile 691: Zeile 650:
writer.Write("|-\n| rowspan=\"{0}\" | {1} |", similarStudios.Count, targetStudio);
writer.Write("|-\n| rowspan=\"{0}\" | {1} |", similarStudios.Count, targetStudio);


foreach (string similarName in similarStudios.OrderBy(item => item.Value).Select(item => item.Key))
foreach (var similarName in similarStudios.OrderBy(item => item.Value).Select(item => item.Key))
{
{
writer.WriteLine("| {0} || {1}\r\n|-", similarName,
writer.WriteLine("| {0} || {1}\r\n|-", similarName,
string.Join(", ", combinedStudioList[similarName].Select(list => "[[Liste der Anime-Titel/" + list + "|" + list + "]]")));
string.Join(", ", combinedStudioList[similarName].Select(list => "[[Liste der Anime-Titel/" + list + "|" + list + "]]")));
}
}
}
}
Zeile 703: Zeile 662:
}
}


static int LevenshteinDistance(string source, string target)
private static int LevenshteinDistance(string source, string target)
{
{
if (string.IsNullOrEmpty(source) && string.IsNullOrEmpty(target)) return 0;
if (string.IsNullOrEmpty(source) && string.IsNullOrEmpty(target))
{
if (string.IsNullOrEmpty(source)) return target.Length;
if (string.IsNullOrEmpty(target)) return source.Length;
return 0;
}
if (string.IsNullOrEmpty(source))
{
return target.Length;
}
if (string.IsNullOrEmpty(target))
{
return source.Length;
}


int[,] d = new int[source.Length + 1, target.Length + 1];
var d = new int[source.Length + 1, target.Length + 1];


for (int i = 1; i <= source.Length; i++) d[i, 0] = i;
for (var i = 1; i <= source.Length; i++)
{
for (int j = 1; j <= target.Length; j++) d[0, j] = j;
d[i, 0] = i;
}
for (var j = 1; j <= target.Length; j++)
{
d[0, j] = j;
}


for (int j = 1; j <= target.Length; j++)
for (var j = 1; j <= target.Length; j++)
{
{
for (int i = 1; i <= source.Length; i++)
for (var i = 1; i <= source.Length; i++)
{
{
if (source[i - 1] == target[j - 1])
if (source[i - 1] == target[j - 1])
{
d[i, j] = d[i - 1, j - 1];
d[i, j] = d[i - 1, j - 1];
}
else
else
d[i, j] = new int[] {
{
d[i, j] = new[]
d[i - 1, j] + 1, // Löschung
{
d[i - 1, j] + 1, // Löschung
d[i, j - 1] + 1, // Einfügung
d[i, j - 1] + 1, // Einfügung
d[i - 1, j - 1] + 1 // Ersetzung
d[i - 1, j - 1] + 1 // Ersetzung
}.Min();
}.Min();
}
}
}
}
}


return d[source.Length, target.Length];
return d[source.Length, target.Length];
}

struct ContentAndTimestamp
{
public string Content;
public string Timestamp;
}
}


Zeile 741: Zeile 714:
{
{
Console.Write("Benutzer: ");
Console.Write("Benutzer: ");
string user = Console.ReadLine();
var user = Console.ReadLine();
Console.Write("Passwort: ");
Console.Write("Passwort: ");
string password = string.Empty;
var password = string.Empty;
// Sternchen zeigen, statt eingetipptem Passwort
// Sternchen zeigen, statt eingetipptem Passwort
while (true)
while (true)
{
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
var keyInfo = Console.ReadKey(true);
if (keyInfo.Key == ConsoleKey.Enter)
if (keyInfo.Key == ConsoleKey.Enter)
{
{
Zeile 753: Zeile 726:
break;
break;
}
}
else if (keyInfo.Key == ConsoleKey.Backspace)
if (keyInfo.Key == ConsoleKey.Backspace)
{
{
if (password.Length > 0)
if (password.Length > 0)
Zeile 772: Zeile 745:
}
}


static void Main(string[] args)
private static void Main(string[] args)
{
{
string matchStudiosFilename = null;
string matchStudiosFilename = null;
NetworkCredential credentials = null;
NetworkCredential credentials = null;

#region Kommandozeilenparameter auswerten
#region Kommandozeilenparameter auswerten

string user = null, password = null;
string user = null, password = null;
for (int i = 0; i < args.Length; i++)
for (var i = 0; i < args.Length; i++)
{
{
if (args[i] == "-?")
if (args[i] == "-?")
Zeile 790: Zeile 765:
return;
return;
}
}
else
if (i + 1 < args.Length)
if (i + 1 < args.Length)
{
{
switch (args[i])
switch (args[i])
Zeile 815: Zeile 789:
}
}
if (!string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(password)) { credentials = new NetworkCredential(user, password); }
if (!string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(password)) { credentials = new NetworkCredential(user, password); }

#endregion
#endregion


wiki = new Mediawiki();
try
try
{
{
Zeile 824: Zeile 798:


#region Eingabelisten lesen
#region Eingabelisten lesen

Console.WriteLine("Alphabetische Listen lesen:");
Console.WriteLine("Alphabetische Listen lesen:");
Console.Write(" ");
Console.Write(" ");
Dictionary<string, ContentAndTimestamp> alphabeticLists = new Dictionary<string, ContentAndTimestamp>();
var alphabeticLists = new Dictionary<string, ContentAndTimestamp>();
Parallel.ForEach(alphaList, letter =>
Parallel.ForEach(alphaList, letter =>
{
{
Mediawiki.TraverseHttpWebResponse(Mediawiki.HttpGet("de", new[,] {
Mediawiki.TraverseHttpWebResponse(Mediawiki.HttpGet("de", new[,]
{ "action", "query" },
{
{ "action", "query" },
{ "prop", "revisions" },
{ "prop", "revisions" },
{ "titles", "Liste der Anime-Titel/" + letter },
{ "titles", "Liste der Anime-Titel/" + letter },
{ "rvprop", "content|timestamp" } }),
{ "rvprop", "content|timestamp" }
delegate (XmlReader reader)
}),
{
delegate(XmlReader reader)
{
if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "rev"))
if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "rev"))
{
{
string curTimestamp = reader.GetAttribute("timestamp");
var curTimestamp = reader.GetAttribute("timestamp");
string content = reader.ReadString();
var content = reader.ReadString();
lock (alphabeticLists)
lock (alphabeticLists)
{
{
alphabeticLists.Add(letter, new ContentAndTimestamp() { Content = content, Timestamp = curTimestamp });
alphabeticLists.Add(letter, new ContentAndTimestamp { Content = content, Timestamp = curTimestamp });
Console.Write(letter);
Console.Write(letter);
}
}
Zeile 849: Zeile 826:
});
});
Console.WriteLine();
Console.WriteLine();

#endregion
#endregion


Zeile 854: Zeile 832:
{
{
#region Studionamen ersetzen
#region Studionamen ersetzen

Console.WriteLine("\nErsetzungen:");
Console.WriteLine("\nErsetzungen:");
Dictionary<Regex, string> replacers = new Dictionary<Regex, string>();
var replacers = new Dictionary<Regex, string>();
foreach (string line in File.ReadAllLines(matchStudiosFilename, Encoding.UTF8))
foreach (var line in File.ReadAllLines(matchStudiosFilename, Encoding.UTF8))
{
{
string[] items = line.Split('\t');
var items = line.Split('\t');
if (items.Length >= 2) replacers.Add(new Regex(@"(\|\s*s[0-9]+\s*=\s*\[*)" + Regex.Escape(items[0]) + @"(\s*[\|}\]])", RegexOptions.Singleline | RegexOptions.CultureInvariant), items[1]);
if (items.Length >= 2)
{
replacers.Add(new Regex(@"(\|\s*s[0-9]+\s*=\s*\[*)" + Regex.Escape(items[0]) + @"(\s*[\|}\]])", RegexOptions.Singleline | RegexOptions.CultureInvariant), items[1]);
}
Console.WriteLine(" " + items[0] + " -> " + items[1]);
Console.WriteLine(" " + items[0] + " -> " + items[1]);
}
}
Zeile 865: Zeile 847:
if (Console.ReadLine().ToLowerInvariant() == "j")
if (Console.ReadLine().ToLowerInvariant() == "j")
{
{
foreach (string letter in alphabeticLists.Keys.OrderBy(key => key))
foreach (var letter in alphabeticLists.Keys.OrderBy(key => key))
{
{
string content = alphabeticLists[letter].Content;
var content = alphabeticLists[letter].Content;
foreach (Regex replace in replacers.Keys) content = replace.Replace(content, "$1" + replacers[replace] + "$2");
foreach (var replace in replacers.Keys)
{
content = replace.Replace(content, "$1" + replacers[replace] + "$2");
}
WriteContent("Liste der Anime-Titel/" + letter, content, alphabeticLists[letter].Timestamp);
WriteContent("Liste der Anime-Titel/" + letter, content, alphabeticLists[letter].Timestamp);
}
}
}
}

#endregion
#endregion
}
}
Zeile 877: Zeile 863:
{
{
#region Eingabelisten parsen
#region Eingabelisten parsen

List<AnimeItem> animeList = new List<AnimeItem>();
Regex templateRegex = new Regex(@"\{\{\s*Anime\-Listeneintrag.+?}}", RegexOptions.Singleline);
var animeList = new List<AnimeItem>();
var templateRegex = new Regex(@"\{\{\s*Anime\-Listeneintrag.+?}}", RegexOptions.Singleline);
Regex commentRegex = new Regex(@"<!--.+?-->", RegexOptions.Singleline);
var commentRegex = new Regex(@"<!--.+?-->", RegexOptions.Singleline);
foreach (string letter in alphabeticLists.Keys.OrderBy(key => key))
foreach (var letter in alphabeticLists.Keys.OrderBy(key => key))
{
{
Console.WriteLine("Liste " + letter);
Console.WriteLine("Liste " + letter);
string content = commentRegex.Replace(alphabeticLists[letter].Content, "");
var content = commentRegex.Replace(alphabeticLists[letter].Content, "");
foreach (Match match in templateRegex.Matches(content))
foreach (Match match in templateRegex.Matches(content))
{
{
Dictionary<string, string> entry = ParseTemplate(match.Value);
var entry = ParseTemplate(match.Value);
AnimeItem anime = AnimeItem.Parse(entry, letter);
var anime = AnimeItem.Parse(entry, letter);
if (anime != null)
if (anime != null)
{
{
Zeile 893: Zeile 880:
//Console.WriteLine(" " + anime);
//Console.WriteLine(" " + anime);
}
}
else Console.Error.WriteLine("Nicht auswertbarer Eintrag: " + match.Value);
else
{
Console.Error.WriteLine("Nicht auswertbarer Eintrag: " + match.Value);
}
}
}
}
}

#endregion
#endregion


// Jahreslisten ergänzen
// Jahreslisten ergänzen
int maxYear = animeList.Select(anime => anime.Year).Max();
var maxYear = animeList.Select(anime => anime.Year).Max();
if (maxYear > DateTime.Now.Year + 1)
if (maxYear > DateTime.Now.Year + 1)
{
{
Console.Write("Das höchste gefundene Jahr ist " + maxYear + ". Jahreslisten bis dahin erstellen [j/n]: ");
Console.Write("Das höchste gefundene Jahr ist " + maxYear + ". Jahreslisten bis dahin erstellen [j/n]: ");
if (Console.ReadLine().ToLowerInvariant() != "j") return;
if (Console.ReadLine().ToLowerInvariant() != "j")
{
return;
}
}
}
for (int year = 2000; year <= maxYear; year = year + 2)
for (var year = 2000; year <= maxYear; year = year + 2)
{
{
yearList.Add(year);
yearList.Add(year);
yearList.Add(year + 1);
yearList.Add(year + 1);
}
}
for (int yearIdx = 0; yearIdx < yearList.Count / 2; yearIdx++)
for (var yearIdx = 0; yearIdx < yearList.Count / 2; yearIdx++)
{
{
int startYear = yearList[yearIdx * 2];
var startYear = yearList[yearIdx * 2];
int endYear = yearList[yearIdx * 2 + 1];
var endYear = yearList[yearIdx * 2 + 1];


TextWriter yearWriter = new StringWriter();
TextWriter yearWriter = new StringWriter();
if (BuildAnimeYearList(animeList, startYear, endYear, yearWriter))
if (BuildAnimeYearList(animeList, startYear, endYear, yearWriter))
{
WriteContent("Liste der Anime-Titel (" + startYear + "–" + endYear + ")", yearWriter.ToString());
WriteContent("Liste der Anime-Titel (" + startYear + "–" + endYear + ")", yearWriter.ToString());
}
}
}


TextWriter studioWriter = new StringWriter();
TextWriter studioWriter = new StringWriter();
if (BuildAnimeStudioList(animeList, studioWriter)) WriteContent("Vorlage:Animestudio Werksliste", studioWriter.ToString());
if (BuildAnimeStudioList(animeList, studioWriter))
{
WriteContent("Vorlage:Animestudio Werksliste", studioWriter.ToString());
}


TextWriter maintainenceWriter = new StringWriter();
TextWriter maintainenceWriter = new StringWriter();
if (BuildMaintainenceInfo(animeList, maintainenceWriter)) WriteContent("Benutzer:Mps/Test", maintainenceWriter.ToString());
if (BuildMaintainenceInfo(animeList, maintainenceWriter))
{
WriteContent("Benutzer:Mps/Test", maintainenceWriter.ToString());
}
}
}
Console.WriteLine("Fertig.");
Console.WriteLine("Fertig.");
Zeile 932: Zeile 934:
Console.Error.WriteLine("Mediawiki-Fehler: " + ex.Message);
Console.Error.WriteLine("Mediawiki-Fehler: " + ex.Message);
}
}
finally
}

public class AnimeItem
{
public List<string> AlternativeTitles;
public string EpisodeCount;
public string JapaneseTitle;
public string JapaneseTranscription;
public int Level;
public string Link;
public string List;
public List<string> Studios;
public string Title;
public string Type;
public int Year;

public static AnimeItem Parse(Dictionary<string, string> parsedTemplate, string list)
{
{
//wiki.Logout();
var result = new AnimeItem();
try
{
result.List = list;
result.Level = int.Parse(parsedTemplate["1"]);
result.Year = int.Parse(parsedTemplate["2"]);
result.Type = parsedTemplate["3"].Trim();
result.EpisodeCount = parsedTemplate["4"].Trim();
result.Title = parsedTemplate["5"].Trim();
if (parsedTemplate.TryGetValue("link", out result.Link))
{
result.Link = result.Link.Trim();
}
if (parsedTemplate.ContainsKey("j"))
{
result.JapaneseTitle = parsedTemplate["j"].Trim();
}
else
{
result.JapaneseTitle = "";
}
if (parsedTemplate.ContainsKey("jt"))
{
result.JapaneseTranscription = parsedTemplate["jt"].Trim();
}
else
{
result.JapaneseTranscription = "";
}
result.AlternativeTitles = new List<string>();
result.Studios = new List<string>();
for (var i = 1; i <= 10; i++)
{
var paramName = "a" + i;
if (parsedTemplate.ContainsKey(paramName))
{
result.AlternativeTitles.Add(parsedTemplate[paramName].Trim());
}
paramName = "s" + i;
if (parsedTemplate.ContainsKey(paramName))
{
result.Studios.Add(parsedTemplate[paramName].Trim());
}
}
return result;
}
catch (KeyNotFoundException)
{
return null;
}
catch (FormatException)
{
return null;
}
}

public string GetWikilink()
{
var result = "[[";
if (!string.IsNullOrEmpty(Link))
{
result += Link + "|";
}
result += Title + "]]";
return result;
}

public static string GetWikiArticle(string wikilink)
{
if (string.IsNullOrEmpty(wikilink))
{
return "";
}
// entferne Wikilinkklammern und splitte an der ersten Pipe
var link = wikilink.TrimStart('[').TrimEnd(']').Split(new[] { '|' }, 2)[0];
// erstes Element (Zielartikel) mit Großbuchstaben beginnen lassen und zurückgeben
link = char.ToUpperInvariant(link[0]) + link.Substring(1);
return link;
}

public string GetYearListEntry()
{
var sb = new StringBuilder();
sb.Append("{{Anime-Listeneintrag|");
sb.Append(0);
sb.Append("|");
sb.Append(Year);
sb.Append("|");
sb.Append(Type);
sb.Append("|");
sb.Append(EpisodeCount);
sb.Append("|");
sb.Append(Title);
if (!string.IsNullOrEmpty(Link))
{
sb.Append("|link=");
sb.Append(Link);
}
if (!string.IsNullOrEmpty(JapaneseTitle))
{
sb.Append("|j=");
sb.Append(JapaneseTitle);
}
if (!string.IsNullOrEmpty(JapaneseTranscription))
{
sb.Append("|jt=");
sb.Append(JapaneseTranscription);
}
for (var aIdx = 0; aIdx < AlternativeTitles.Count; aIdx++)
{
sb.Append("|a");
sb.Append(aIdx + 1);
sb.Append("=");
sb.Append(AlternativeTitles[aIdx]);
}
for (var sIdx = 0; sIdx < Studios.Count; sIdx++)
{
sb.Append("|s");
sb.Append(sIdx + 1);
sb.Append("=");
sb.Append(Studios[sIdx]);
}
sb.Append("|mark=year}}");
return sb.ToString();
}

public override string ToString()
{
return Title;
}
}
#if (DEBUG)
Console.ReadLine();
#endif
}
}

private struct ContentAndTimestamp
{
public string Content;
public string Timestamp;
}

#region Konfiguration

private static readonly UnicodeCategory[] validCharTypes =
{
UnicodeCategory.UppercaseLetter, UnicodeCategory.LowercaseLetter, UnicodeCategory.TitlecaseLetter, UnicodeCategory.ModifierLetter, UnicodeCategory.OtherLetter,
UnicodeCategory.DecimalDigitNumber, UnicodeCategory.LetterNumber, UnicodeCategory.OtherNumber, UnicodeCategory.SpaceSeparator
};

private static readonly string[] alphaList = { "0–9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" };
private static readonly List<int> yearList = new List<int> { 1900, 1945, 1946, 1962, 1963, 1969, 1970, 1979, 1980, 1984, 1985, 1989, 1990, 1994, 1995, 1997, 1998, 1999 }; // ab 2000 wird automatisch generiert

private static readonly Dictionary<string, string> animeTypes = new Dictionary<string, string>
{
{ "F", "Film" },
{ "S", "Fernsehserie" },
{ "W", "Web-Anime" },
{ "O", "OVA" },
{ "SP", "Special" },
{ "K", "Kurzfilm" }
};

#endregion
}
}
}
}

Version vom 13. März 2017, 22:07 Uhr

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;

namespace AnimeListenUpdater
{

  #region Exceptions

  internal class BusinessException : Exception
  {
    public BusinessException(string message)
      : base(message)
    {
    }
  }

  internal class MediawikiException : BusinessException
  {
    public MediawikiException(string message)
      : base(message)
    {
    }

    public MediawikiException(string code, string info)
      : base("Mediawiki error \"" + code + ": " + info + "\"")
    {
      Code = code;
    }

    public string Code { get; }
  }

  internal class MediawikiLagException : MediawikiException
  {
    public MediawikiLagException(string code, string info)
      : base("Mediawiki error \"" + code + ": " + info + "\"")
    {
      LagTime = GetLagTime(info);
    }

    public int LagTime { get; private set; }

    public static int GetLagTime(string info)
    {
      var lagtime = -1;
      var match = Regex.Match(info, "Waiting for [^ ]*: ([0-9.-]+) seconds lagged");
      if (match.Success)
      {
        int.TryParse(match.Groups[1].Value, out lagtime);
      }
      return lagtime;
    }
  }

  #endregion

  public static class Mediawiki
  {
    #region Netzwerk- und Mediawiki-Funktionen

    private static readonly CookieContainer cookieContainer = new CookieContainer();
    private static readonly string userAgentString = GetUserAgentString();

    private static string GetUserAgentString()
    {
      var assemblyName = Assembly.GetExecutingAssembly().GetName();

      // Programmname und -version
      var userAgent = assemblyName.Name + "/" + assemblyName.Version.Major + "." + assemblyName.Version.Minor;
      // Umgebungsinformationen (OS, Laufzeitumgebung)
      userAgent += " (" + Environment.OSVersion.VersionString + "; .NET CLR " + Environment.Version + ")";

      return userAgent;
    }

    private static string EncodeQuery(string[,] query)
    {
      var result = "";
      for (var i = 0; i < query.GetLength(0); i++)
      {
        result += "&" + query[i, 0];
        var value = query[i, 1];
        if (value != null)
        {
          result += "=" + Uri.EscapeDataString(value);
        }
      }
      return result;
    }

    private static string MultipartQuery(string[,] postData, string boundaryIdentifier)
    {
      var sb = new StringBuilder();
      for (var i = 0; i < postData.GetLength(0); i++)
      {
        sb.Append("--");
        sb.Append(boundaryIdentifier);
        sb.Append("\r\nContent-Disposition: form-data; name=\"");
        sb.Append(postData[i, 0]);
        sb.Append("\"\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n");
        sb.Append(postData[i, 1]);
        sb.Append("\r\n");
      }
      return sb.ToString();
    }

    private static HttpWebRequest GenerateHttpWebRequest(string language, string[,] query)
    {
      var url = "https://";
      if (language == "wikidata")
      {
        url += "www.wikidata.org";
      }
      else
      {
        url += language + ".wikipedia.org";
      }
      url += "/w/api.php?format=xml";
      url += EncodeQuery(query);

      var request = (HttpWebRequest)WebRequest.Create(url);

      request.UserAgent = userAgentString;
      request.CookieContainer = cookieContainer;
      request.AutomaticDecompression = DecompressionMethods.GZip;
      request.KeepAlive = true;
      return request;
    }

    public static HttpWebResponse HttpGet(string language, string[,] query)
    {
      var request = GenerateHttpWebRequest(language, query);
      return (HttpWebResponse)request.GetResponse();
    }

    public static HttpWebResponse HttpPost(string language, string[,] query, string[,] postData)
    {
      var request = GenerateHttpWebRequest(language, query);
      request.Method = "POST";
      if (postData != null)
      {
        var boundary = Guid.NewGuid().ToString();
        var postContent = new UTF8Encoding(false).GetBytes(MultipartQuery(postData, boundary));
        request.ContentType = "multipart/form-data; boundary=" + boundary;
        using (var stream = request.GetRequestStream())
        {
          stream.Write(postContent, 0, postContent.Length);
        }
      }
      return (HttpWebResponse)request.GetResponse();
    }

    public static void TraverseHttpWebResponse(HttpWebResponse response, Action<XmlReader> onXmlNode)
    {
      using (response)
      {
        using (var stream = response.GetResponseStream())
        {
          using (var reader = XmlReader.Create(stream))
          {
            if (!reader.ReadToFollowing("api"))
            {
              throw new MediawikiException("Malformed response");
            }

            while (reader.Read())
            {
              CheckForError(reader);
              onXmlNode(reader);
            }
          }
        }
      }
    }

    public static string CalcMd5Hash(byte[] data)
    {
      MD5 md5 = new MD5CryptoServiceProvider();
      var md5Hash = md5.ComputeHash(data);
      return BitConverter.ToString(md5Hash).Replace("-", "").ToLowerInvariant();
    }

    public static void CheckForError(XmlReader reader)
    {
      if (reader.NodeType == XmlNodeType.Element)
      {
        if (reader.LocalName == "warnings")
        {
          Debug.WriteLine("  warning: " + reader.ReadInnerXml());
        }
        else if (reader.LocalName == "error")
        {
          var code = reader.GetAttribute("code");
          var info = reader.GetAttribute("info");
          Debug.WriteLine(" error \"" + code + "\": " + info);
          if (code == "maxlag")
          {
            throw new MediawikiLagException(code, info);
          }
          throw new MediawikiException(code, info);
        }
      }
    }

    public enum TokenType
    {
      Csrf,
      Watch,
      Patrol,
      Rollback,
      UserRights,
      Login,
      CreateAccount
    }

    public static string GetToken(string lang, TokenType tokenType)
    {
      var tokenName = tokenType.ToString().ToLowerInvariant();

      string[,] paramList =
      {
        { "action", "query" },
        { "meta", "tokens" },
        { "type", tokenName }
      };

      string token = null;
      TraverseHttpWebResponse(HttpPost(lang, paramList, null), reader =>
      {
        if ((reader.NodeType == XmlNodeType.Element) && (reader.LocalName == "tokens"))
        {
          token = reader.GetAttribute(tokenName + "token");
        }
      });
      return token;
    }

    public static void Login(string lang, NetworkCredential credentials)
    {
      string[,] post =
      {
        { "lgpassword", credentials.Password },
        { "lgtoken", GetToken(lang, TokenType.Login) }
      };
      string[,] paramList =
      {
        { "action", "login" },
        { "lgname", credentials.UserName }
      };

      TraverseHttpWebResponse(HttpPost(lang, paramList, post), reader =>
      {
        if ((reader.NodeType == XmlNodeType.Element) && (reader.LocalName == "login"))
        {
          var result = reader.GetAttribute("result");
          if (result != "Success")
          {
            if (result == "Throttled")
            {
              result += $" (Please wait {reader.GetAttribute("wait")}s)";
            }
            throw new MediawikiException(result);
          }
        }
      });
    }

    public static void Logout(string lang)
    {
      using (HttpGet(lang, new[,] { { "action", "logout" } })) { }
    }

    #endregion
  }

  public class Program
  {
    private static string editToken;

    private static Dictionary<string, string> ParseTemplate(string template)
    {
      var result = new Dictionary<string, string>();

      var startPos = template.IndexOf("{{", StringComparison.Ordinal);
      var lastPos = template.LastIndexOf("}}", StringComparison.Ordinal);
      if ((startPos >= 0) && (lastPos >= 0))
      {
        var split = template.Substring(startPos + 2, lastPos - startPos - 2).Split('|');
        var parameters = new List<string>();

        // Wikilinks wieder zusammenführen, d.h. "[[A", "B]]" zu "[[A|B]]".
        var doubleBracketCount = 0;
        for (var i = 0; i < split.Length; i++)
        {
          if (doubleBracketCount == 0)
          {
            parameters.Add(split[i]);
          }
          else
          {
            parameters[parameters.Count - 1] += "|" + split[i];
          }

          var curIdx = 0;
          while (curIdx >= 0)
          {
            curIdx = split[i].IndexOf("[[", curIdx, StringComparison.Ordinal);
            if (curIdx >= 0)
            {
              doubleBracketCount++;
              curIdx += 2;
            }
          }
          curIdx = 0;
          while (curIdx >= 0)
          {
            curIdx = split[i].IndexOf("]]", curIdx, StringComparison.Ordinal);
            if (curIdx >= 0)
            {
              doubleBracketCount--;
              curIdx += 2;
            }
          }
        }

        for (var paramIdx = 0; paramIdx < parameters.Count; paramIdx++)
        {
          var keyValue = parameters[paramIdx].Split(new[] { '=' }, 2);
          try
          {
            if (keyValue.Length == 2)
            {
              result.Add(keyValue[0], keyValue[1]);
            }
            else
            {
              result.Add(paramIdx.ToString(), parameters[paramIdx]);
            }
          }
          catch (ArgumentException)
          {
            Console.Error.WriteLine("Doppelte Parameter: " + template);
          }
        }
      }
      return result;
    }

    // zur Sortierung alle Zeichen außer Buchstaben, Zahlen und Leerzeichen entfernen und Buchstaben in Kleinschreibung umwandeln
    private static string RemoveDiacriticsAndSpecialChars(string text)
    {
      if (text.StartsWith("The ") || text.StartsWith("Der ") || text.StartsWith("Die ") || text.StartsWith("Das "))
      {
        text = text.Substring(4);
      }

      return string.Concat(text.Normalize(NormalizationForm.FormKD).Where(ch => validCharTypes.Contains(CharUnicodeInfo.GetUnicodeCategory(ch)))).Normalize(NormalizationForm.FormKC).ToLowerInvariant();
    }

    private static void WriteContent(string pagetitle, string content, string basetimestamp = null)
    {
      if (editToken == null)
      {
        editToken = Mediawiki.GetToken("de", Mediawiki.TokenType.Csrf);
      }

      bool badToken;
      do
      {
        badToken = false;
        try
        {
          Console.Write("Schreibe " + pagetitle);
          var md5Hash = Mediawiki.CalcMd5Hash(new UTF8Encoding(false).GetBytes(content));

          var post = new string[3 + (basetimestamp != null ? 1 : 0), 2];
          post[0, 0] = "token";
          post[0, 1] = editToken;
          post[1, 0] = "text";
          post[1, 1] = content;
          post[2, 0] = "md5";
          post[2, 1] = md5Hash;
          if (basetimestamp != null)
          {
            post[3, 0] = "basetimestamp";
            post[3, 1] = basetimestamp;
          }

          using (var response = Mediawiki.HttpPost("de", new[,]
            {
              { "assert", "user" },
              { "action", "edit" },
              { "title", pagetitle },
              { "notminor", null },
              { "summary", "Listen-Aktualisierung per [[Benutzer:Mps/AnimeListenUpdater.cs]]" }
            },
            post))
          {
            using (var stream = response.GetResponseStream())
            {
              using (var reader = XmlReader.Create(stream))
              {
                while (reader.Read())
                {
                  Mediawiki.CheckForError(reader);
                }
              }
            }
          }
          Console.WriteLine(".");
        }
        catch (MediawikiException mwe)
        {
          Console.Error.WriteLine("\n" + pagetitle + ": " + mwe.Message);
          if (mwe.Code == "badtoken")
          {
            // Die API wirft manchmal "badtoken", daher einfach Edit nochmal probieren.
            Console.Error.WriteLine("  Neuer Versuch.");
            badToken = true;
          }
        }
      } while (badToken);
    }

    private static bool BuildAnimeYearList(List<AnimeItem> animeList, int startYear, int endYear, TextWriter writer)
    {
      writer.WriteLine("__ABSCHNITTE_NICHT_BEARBEITEN__ __KEIN_INHALTSVERZEICHNIS__");
      writer.WriteLine("Dies ist eine '''chronologisch sortierte Liste der Anime-Titel''' von " + startYear + " bis " + endYear + ".");
      writer.WriteLine("<!--");
      writer.WriteLine("##########");
      writer.WriteLine("Bitte editiere diese Liste _nicht_, da sie von einem Bot automatisch generiert wird, der durchgeführte Änderungen überschreibt. Stattdessen können und sollten die alphabetisch sortierten Listen der Liste der Anime-Titel http://de.wikipedia.org/wiki/Liste_der_Anime-Titel bearbeitet werden, aus denen diese Liste generiert wird.");
      writer.WriteLine("##########");
      writer.WriteLine("-->");
      writer.WriteLine("{{Navigationsleiste Liste der Anime-Titel}}");
      writer.WriteLine();

      var hasAtLeastOneYear = false;
      for (var year = startYear; year <= endYear; year++)
      {
        var yearClosure = year;
        IEnumerable<AnimeItem> animesInYear = from anime in animeList
          where anime.Year == yearClosure
          orderby RemoveDiacriticsAndSpecialChars(anime.Title)
          select anime;

        if (animesInYear.Any())
        {
          hasAtLeastOneYear = true;
          writer.WriteLine("== " + year + " ==");
          writer.WriteLine("{{Anime-Listenkopf|");
          foreach (var anime in animesInYear)
          {
            writer.WriteLine(anime.GetYearListEntry());
          }
          writer.WriteLine("|mark=year}}");
          writer.WriteLine();
        }
      }
      writer.WriteLine("[[Kategorie:Liste (Anime)|#" + startYear + "]]");

      return hasAtLeastOneYear;
    }

    private static bool BuildAnimeStudioList(List<AnimeItem> animeList, TextWriter writer)
    {
      IEnumerable<string> linkedStudios = animeList.SelectMany(anime => anime.Studios) // Studiolisten zusammenführen
        .Where(studio => studio.StartsWith("[[")) // nach verlinkten Studios filtern
        .Select(studio => AnimeItem.GetWikiArticle(studio)) // Linkklammern entfernen, nach Pipe separieren und nur erstes Element (Linkziel) zurückgeben
        .Distinct() // doppelte Einträge entfernen
        .OrderBy(name => RemoveDiacriticsAndSpecialChars(name)); // sortieren

      foreach (var curStudio in linkedStudios)
      {
        writer.WriteLine("{{#ifeq: {{{1}}}|" + curStudio + "|");

        var animesOfStudio = animeList
          .Where(anime => anime.Studios.Any(studio => AnimeItem.GetWikiArticle(studio) == curStudio)) // nach Anime die vom aktuellen Studio gefertigt wurden filtern
          .OrderBy(anime => anime.Year + RemoveDiacriticsAndSpecialChars(anime.Title)) // nach Jahr und Titel sortieren
          .GroupBy(anime => anime.Type) // nach Typ gruppieren
          .ToDictionary(group => group.Key, group => group.ToList()); // in assoziative Liste überführen

        // Generierungsregeln:
        // ab 10 Einträge pro Typ: eigene Gruppe (Reihenfolge: Fernsehserien, Filme, Original Video Animations, Specials, Kurzfilme, Weitere Anime-Produktionen)
        // ab 10 Einträge pro Gruppe: zwei Spalten, wobei bei ungerader Anzahl die erste Spalte länger ist
        var hasCaptions = false;
        foreach (var animeType in animeTypes.Keys)
        {
          #region wenn ein Studio mehr als 10 Animes eines Typs produziert hat, diese separat aufführen

          if (animesOfStudio.ContainsKey(animeType) && (animesOfStudio[animeType].Count >= 10))
          {
            hasCaptions = true;
            switch (animeType)
            {
              case "S":
                writer.WriteLine("=== Fernsehserien ===");
                break;
              case "F":
                writer.WriteLine("=== Filme ===");
                break;
              case "W":
                writer.WriteLine("=== Web-Animes ===");
                break;
              case "O":
                writer.WriteLine("=== Original Video Animations ===");
                break;
              case "SP":
                writer.WriteLine("=== Specials ===");
                break;
              case "K":
                writer.WriteLine("=== Kurzfilme ===");
                break;
            }
            writer.WriteLine("<div style=\"{{Spaltenbreite}}\">");
            foreach (var anime in animesOfStudio[animeType])
            {
              writer.WriteLine("* " + anime.Year + ": ''" + anime.GetWikilink() + "''");
            }
            writer.WriteLine("</div>");

            animesOfStudio.Remove(animeType);
          }

          #endregion
        }

        IEnumerable<AnimeItem> otherAnimes = animesOfStudio
          .SelectMany(kvp => kvp.Value)
          .OrderBy(anime => anime.Year + RemoveDiacriticsAndSpecialChars(anime.Title));

        if (otherAnimes.Any())
        {
          if (hasCaptions)
          {
            writer.WriteLine("=== Weitere Anime-Produktionen ===");
          }
          writer.WriteLine("<div style=\"{{Spaltenbreite}}\">");
          foreach (var anime in otherAnimes)
          {
            string desc;
            if (animeTypes.ContainsKey(anime.Type))
            {
              desc = " (" + animeTypes[anime.Type] + ")";
            }
            else
            {
              desc = "";
            }
            writer.WriteLine("* " + anime.Year + ": ''" + anime.GetWikilink() + "''" + desc);
          }
          writer.Write("</div>");
        }
        writer.Write("}}");
      }

      writer.Write("<div class=\"noprint\" style=\"padding-left:1em;\"><small>''Diese Liste wird aus den Einträgen der [[Liste der Anime-Titel]] generiert. Einträge können und sollten dort ergänzt werden.</small></div>");
      writer.Write("<noinclude>[[Kategorie:Vorlage:Film und Fernsehen]]</noinclude>");

      return true;
    }

    private static bool BuildMaintainenceInfo(List<AnimeItem> animeList, TextWriter writer)
    {
      var allStudios = animeList.SelectMany(anime => anime.Studios, (anime, studio) => new { Name = studio, anime.List }).ToList();

      var linkedStudios = allStudios
        .Where(studio => studio.Name.StartsWith("[["))
        .Select(studio => new { Name = AnimeItem.GetWikiArticle(studio.Name), studio.List })
        .GroupBy(studio => studio.Name)
        .Select(studio => new { Name = studio.Key, Count = studio.Count(), Lists = studio.Select(list => list.List).Distinct().OrderBy(list => list) })
        .OrderBy(studio => studio.Name);

      var unlinkedStudios = allStudios
        .Where(studio => !studio.Name.StartsWith("[["))
        .Select(studio => new { studio.Name, studio.List })
        .GroupBy(studio => studio.Name)
        .Select(studio => new { Name = studio.Key, Count = studio.Count(), Lists = studio.Select(list => list.List).Distinct().OrderBy(list => list) })
        .ToList();

      writer.WriteLine("== Nur teilweise verlinkte Studios ==");
      writer.WriteLine("{| class=\"wikitable sortable\"");
      writer.WriteLine("! Studio || Auftreten || Listen");
      foreach (var linkedStudio in linkedStudios)
      {
        var match = unlinkedStudios.FirstOrDefault(unlinkedStudio => unlinkedStudio.Name == linkedStudio.Name);

        if (match != null)
        {
          writer.WriteLine("|-\n| {0} || {1} || {2}", linkedStudio.Name, match.Count, string.Join(", ", match.Lists.Select(list => "[[Liste der Anime-Titel/" + list + "|" + list + "]]")));
        }
      }
      writer.WriteLine("|}\n");

      writer.WriteLine("== Potentiell verlinkbare Studios ==");
      writer.WriteLine("{| class=\"wikitable sortable\"");
      writer.WriteLine("! Studio || Anzahl || Listen");
      foreach (var unlinkedStudio in unlinkedStudios)
      {
        if (!string.IsNullOrEmpty(unlinkedStudio.Name) && (unlinkedStudio.Count > 5) && linkedStudios.All(linkedStudio => linkedStudio.Name != unlinkedStudio.Name))
        {
          writer.WriteLine("|-\n| {0} || {1} || {2}", unlinkedStudio.Name, unlinkedStudio.Count, string.Join(", ", unlinkedStudio.Lists.Select(list => "[[Liste der Anime-Titel/" + list + "|" + list + "]]")));
        }
      }
      writer.WriteLine("|}\n");

      var combinedStudioList = allStudios
        .Where(studio => !string.IsNullOrEmpty(studio.Name))
        .Select(studio => new { Name = AnimeItem.GetWikiArticle(studio.Name), studio.List })
        .GroupBy(studio => studio.Name)
        .OrderBy(studio => studio.Key)
        .ToDictionary(group => group.Key, group => group.Select(studio => studio.List).Distinct().ToList());

      writer.WriteLine("== Ähnliche Studionamen ==");
      writer.WriteLine("{| class=\"wikitable sortable\"");
      writer.WriteLine("! Studio || Ähnliche Namen || Listen");
      var allStudiosArray = combinedStudioList.Keys.ToArray();
      for (var i = 0; i < allStudiosArray.Length; i++)
      {
        var targetStudio = allStudiosArray[i];
        var similarStudios = new Dictionary<string, int>();
        for (var j = 0 /*i + 1*/; j < allStudiosArray.Length; j++)
        {
          if (i == j)
          {
            continue;
          }

          var otherStudio = allStudiosArray[j];

          var dist = LevenshteinDistance(RemoveDiacriticsAndSpecialChars(targetStudio), RemoveDiacriticsAndSpecialChars(otherStudio));
          if (dist <= 2)
          {
            similarStudios.Add(otherStudio, dist);
          }
        }
        if (similarStudios.Count > 0)
        {
          writer.Write("|-\n| rowspan=\"{0}\" | {1} |", similarStudios.Count, targetStudio);

          foreach (var similarName in similarStudios.OrderBy(item => item.Value).Select(item => item.Key))
          {
            writer.WriteLine("| {0} || {1}\r\n|-", similarName,
              string.Join(", ", combinedStudioList[similarName].Select(list => "[[Liste der Anime-Titel/" + list + "|" + list + "]]")));
          }
        }
      }
      writer.WriteLine("|}");

      return true;
    }

    private static int LevenshteinDistance(string source, string target)
    {
      if (string.IsNullOrEmpty(source) && string.IsNullOrEmpty(target))
      {
        return 0;
      }
      if (string.IsNullOrEmpty(source))
      {
        return target.Length;
      }
      if (string.IsNullOrEmpty(target))
      {
        return source.Length;
      }

      var d = new int[source.Length + 1, target.Length + 1];

      for (var i = 1; i <= source.Length; i++)
      {
        d[i, 0] = i;
      }
      for (var j = 1; j <= target.Length; j++)
      {
        d[0, j] = j;
      }

      for (var j = 1; j <= target.Length; j++)
      {
        for (var i = 1; i <= source.Length; i++)
        {
          if (source[i - 1] == target[j - 1])
          {
            d[i, j] = d[i - 1, j - 1];
          }
          else
          {
            d[i, j] = new[]
            {
              d[i - 1, j] + 1, // Löschung
              d[i, j - 1] + 1, // Einfügung
              d[i - 1, j - 1] + 1 // Ersetzung
            }.Min();
          }
        }
      }

      return d[source.Length, target.Length];
    }

    private static NetworkCredential GetCredentials()
    {
      Console.Write("Benutzer: ");
      var user = Console.ReadLine();
      Console.Write("Passwort: ");
      var password = string.Empty;
      // Sternchen zeigen, statt eingetipptem Passwort
      while (true)
      {
        var keyInfo = Console.ReadKey(true);
        if (keyInfo.Key == ConsoleKey.Enter)
        {
          Console.WriteLine();
          break;
        }
        if (keyInfo.Key == ConsoleKey.Backspace)
        {
          if (password.Length > 0)
          {
            password = password.Remove(password.Length - 1);
            // bei Backspace: Cursor eine Position zurück, mit Leerzeichen überschreiben, wieder eine Position zurück
            Console.Write(keyInfo.KeyChar + " " + keyInfo.KeyChar);
          }
        }
        else
        {
          password += keyInfo.KeyChar;
          Console.Write("*");
        }
      }
      Console.WriteLine();
      return new NetworkCredential(user, password);
    }

    private static void Main(string[] args)
    {
      string matchStudiosFilename = null;
      NetworkCredential credentials = null;

      #region Kommandozeilenparameter auswerten

      string user = null, password = null;
      for (var i = 0; i < args.Length; i++)
      {
        if (args[i] == "-?")
        {
          Console.WriteLine(Assembly.GetExecutingAssembly().GetName() + " [Parameter]");
          Console.WriteLine();
          Console.WriteLine("-user Benutzername      Benutzername angeben");
          Console.WriteLine("-pass Passwort          Passwort angeben");
          Console.WriteLine("-matchstudios Datei     Studionamen in den Jahreslisten angleichen.");
          Console.WriteLine("                        Eingabe ist eine UTF8-kodierte Datei die zeilenweise die Ersetzungen angibt im Format: Ist<TAB>Soll");
          return;
        }
        if (i + 1 < args.Length)
        {
          switch (args[i])
          {
            case "-user":
              user = args[i + 1];
              i++;
              break;
            case "-pass":
              password = args[i + 1];
              i++;
              break;
            case "-matchstudios":
              matchStudiosFilename = args[i + 1];
              if (!File.Exists(matchStudiosFilename))
              {
                Console.WriteLine("Datei nicht gefunden!");
                return;
              }
              break;
          }
        }
      }
      if (!string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(password)) { credentials = new NetworkCredential(user, password); }

      #endregion

      try
      {
        if (credentials == null) { credentials = GetCredentials(); }
        Mediawiki.Login("de", credentials);

        #region Eingabelisten lesen

        Console.WriteLine("Alphabetische Listen lesen:");
        Console.Write("  ");
        var alphabeticLists = new Dictionary<string, ContentAndTimestamp>();
        Parallel.ForEach(alphaList, letter =>
        {
          Mediawiki.TraverseHttpWebResponse(Mediawiki.HttpGet("de", new[,]
            {
              { "action", "query" },
              { "prop", "revisions" },
              { "titles", "Liste der Anime-Titel/" + letter },
              { "rvprop", "content|timestamp" }
            }),
            delegate(XmlReader reader)
            {
              if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "rev"))
              {
                var curTimestamp = reader.GetAttribute("timestamp");
                var content = reader.ReadString();
                lock (alphabeticLists)
                {
                  alphabeticLists.Add(letter, new ContentAndTimestamp { Content = content, Timestamp = curTimestamp });
                  Console.Write(letter);
                }
              }
            });
        });
        Console.WriteLine();

        #endregion

        if (matchStudiosFilename != null)
        {
          #region Studionamen ersetzen

          Console.WriteLine("\nErsetzungen:");
          var replacers = new Dictionary<Regex, string>();
          foreach (var line in File.ReadAllLines(matchStudiosFilename, Encoding.UTF8))
          {
            var items = line.Split('\t');
            if (items.Length >= 2)
            {
              replacers.Add(new Regex(@"(\|\s*s[0-9]+\s*=\s*\[*)" + Regex.Escape(items[0]) + @"(\s*[\|}\]])", RegexOptions.Singleline | RegexOptions.CultureInvariant), items[1]);
            }
            Console.WriteLine("  " + items[0] + " -> " + items[1]);
          }
          Console.Write("Fortfahren [j/n]: ");
          if (Console.ReadLine().ToLowerInvariant() == "j")
          {
            foreach (var letter in alphabeticLists.Keys.OrderBy(key => key))
            {
              var content = alphabeticLists[letter].Content;
              foreach (var replace in replacers.Keys)
              {
                content = replace.Replace(content, "$1" + replacers[replace] + "$2");
              }
              WriteContent("Liste der Anime-Titel/" + letter, content, alphabeticLists[letter].Timestamp);
            }
          }

          #endregion
        }
        else
        {
          #region Eingabelisten parsen

          var animeList = new List<AnimeItem>();
          var templateRegex = new Regex(@"\{\{\s*Anime\-Listeneintrag.+?}}", RegexOptions.Singleline);
          var commentRegex = new Regex(@"<!--.+?-->", RegexOptions.Singleline);
          foreach (var letter in alphabeticLists.Keys.OrderBy(key => key))
          {
            Console.WriteLine("Liste " + letter);
            var content = commentRegex.Replace(alphabeticLists[letter].Content, "");
            foreach (Match match in templateRegex.Matches(content))
            {
              var entry = ParseTemplate(match.Value);
              var anime = AnimeItem.Parse(entry, letter);
              if (anime != null)
              {
                animeList.Add(anime);
                //Console.WriteLine("  " + anime);
              }
              else
              {
                Console.Error.WriteLine("Nicht auswertbarer Eintrag: " + match.Value);
              }
            }
          }

          #endregion

          // Jahreslisten ergänzen
          var maxYear = animeList.Select(anime => anime.Year).Max();
          if (maxYear > DateTime.Now.Year + 1)
          {
            Console.Write("Das höchste gefundene Jahr ist " + maxYear + ". Jahreslisten bis dahin erstellen [j/n]: ");
            if (Console.ReadLine().ToLowerInvariant() != "j")
            {
              return;
            }
          }
          for (var year = 2000; year <= maxYear; year = year + 2)
          {
            yearList.Add(year);
            yearList.Add(year + 1);
          }
          for (var yearIdx = 0; yearIdx < yearList.Count / 2; yearIdx++)
          {
            var startYear = yearList[yearIdx * 2];
            var endYear = yearList[yearIdx * 2 + 1];

            TextWriter yearWriter = new StringWriter();
            if (BuildAnimeYearList(animeList, startYear, endYear, yearWriter))
            {
              WriteContent("Liste der Anime-Titel (" + startYear + "–" + endYear + ")", yearWriter.ToString());
            }
          }

          TextWriter studioWriter = new StringWriter();
          if (BuildAnimeStudioList(animeList, studioWriter))
          {
            WriteContent("Vorlage:Animestudio Werksliste", studioWriter.ToString());
          }

          TextWriter maintainenceWriter = new StringWriter();
          if (BuildMaintainenceInfo(animeList, maintainenceWriter))
          {
            WriteContent("Benutzer:Mps/Test", maintainenceWriter.ToString());
          }
        }
        Console.WriteLine("Fertig.");
      }
      catch (MediawikiException ex)
      {
        Console.Error.WriteLine("Mediawiki-Fehler: " + ex.Message);
      }
    }

    public class AnimeItem
    {
      public List<string> AlternativeTitles;
      public string EpisodeCount;
      public string JapaneseTitle;
      public string JapaneseTranscription;
      public int Level;
      public string Link;
      public string List;
      public List<string> Studios;
      public string Title;
      public string Type;
      public int Year;

      public static AnimeItem Parse(Dictionary<string, string> parsedTemplate, string list)
      {
        var result = new AnimeItem();
        try
        {
          result.List = list;
          result.Level = int.Parse(parsedTemplate["1"]);
          result.Year = int.Parse(parsedTemplate["2"]);
          result.Type = parsedTemplate["3"].Trim();
          result.EpisodeCount = parsedTemplate["4"].Trim();
          result.Title = parsedTemplate["5"].Trim();
          if (parsedTemplate.TryGetValue("link", out result.Link))
          {
            result.Link = result.Link.Trim();
          }
          if (parsedTemplate.ContainsKey("j"))
          {
            result.JapaneseTitle = parsedTemplate["j"].Trim();
          }
          else
          {
            result.JapaneseTitle = "";
          }
          if (parsedTemplate.ContainsKey("jt"))
          {
            result.JapaneseTranscription = parsedTemplate["jt"].Trim();
          }
          else
          {
            result.JapaneseTranscription = "";
          }
          result.AlternativeTitles = new List<string>();
          result.Studios = new List<string>();
          for (var i = 1; i <= 10; i++)
          {
            var paramName = "a" + i;
            if (parsedTemplate.ContainsKey(paramName))
            {
              result.AlternativeTitles.Add(parsedTemplate[paramName].Trim());
            }
            paramName = "s" + i;
            if (parsedTemplate.ContainsKey(paramName))
            {
              result.Studios.Add(parsedTemplate[paramName].Trim());
            }
          }
          return result;
        }
        catch (KeyNotFoundException)
        {
          return null;
        }
        catch (FormatException)
        {
          return null;
        }
      }

      public string GetWikilink()
      {
        var result = "[[";
        if (!string.IsNullOrEmpty(Link))
        {
          result += Link + "|";
        }
        result += Title + "]]";
        return result;
      }

      public static string GetWikiArticle(string wikilink)
      {
        if (string.IsNullOrEmpty(wikilink))
        {
          return "";
        }
        // entferne Wikilinkklammern und splitte an der ersten Pipe
        var link = wikilink.TrimStart('[').TrimEnd(']').Split(new[] { '|' }, 2)[0];
        // erstes Element (Zielartikel) mit Großbuchstaben beginnen lassen und zurückgeben
        link = char.ToUpperInvariant(link[0]) + link.Substring(1);
        return link;
      }

      public string GetYearListEntry()
      {
        var sb = new StringBuilder();
        sb.Append("{{Anime-Listeneintrag|");
        sb.Append(0);
        sb.Append("|");
        sb.Append(Year);
        sb.Append("|");
        sb.Append(Type);
        sb.Append("|");
        sb.Append(EpisodeCount);
        sb.Append("|");
        sb.Append(Title);
        if (!string.IsNullOrEmpty(Link))
        {
          sb.Append("|link=");
          sb.Append(Link);
        }
        if (!string.IsNullOrEmpty(JapaneseTitle))
        {
          sb.Append("|j=");
          sb.Append(JapaneseTitle);
        }
        if (!string.IsNullOrEmpty(JapaneseTranscription))
        {
          sb.Append("|jt=");
          sb.Append(JapaneseTranscription);
        }
        for (var aIdx = 0; aIdx < AlternativeTitles.Count; aIdx++)
        {
          sb.Append("|a");
          sb.Append(aIdx + 1);
          sb.Append("=");
          sb.Append(AlternativeTitles[aIdx]);
        }
        for (var sIdx = 0; sIdx < Studios.Count; sIdx++)
        {
          sb.Append("|s");
          sb.Append(sIdx + 1);
          sb.Append("=");
          sb.Append(Studios[sIdx]);
        }
        sb.Append("|mark=year}}");
        return sb.ToString();
      }

      public override string ToString()
      {
        return Title;
      }
    }

    private struct ContentAndTimestamp
    {
      public string Content;
      public string Timestamp;
    }

    #region Konfiguration

    private static readonly UnicodeCategory[] validCharTypes =
    {
      UnicodeCategory.UppercaseLetter, UnicodeCategory.LowercaseLetter, UnicodeCategory.TitlecaseLetter, UnicodeCategory.ModifierLetter, UnicodeCategory.OtherLetter,
      UnicodeCategory.DecimalDigitNumber, UnicodeCategory.LetterNumber, UnicodeCategory.OtherNumber, UnicodeCategory.SpaceSeparator
    };

    private static readonly string[] alphaList = { "0–9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" };
    private static readonly List<int> yearList = new List<int> { 1900, 1945, 1946, 1962, 1963, 1969, 1970, 1979, 1980, 1984, 1985, 1989, 1990, 1994, 1995, 1997, 1998, 1999 }; // ab 2000 wird automatisch generiert

    private static readonly Dictionary<string, string> animeTypes = new Dictionary<string, string>
    {
      { "F", "Film" },
      { "S", "Fernsehserie" },
      { "W", "Web-Anime" },
      { "O", "OVA" },
      { "SP", "Special" },
      { "K", "Kurzfilm" }
    };

    #endregion
  }
}