Zum Inhalt springen

Benutzer:Mps/AnimeListenUpdater.cs

aus Wikipedia, der freien Enzyklopädie
Dies ist eine alte Version dieser Seite, zuletzt bearbeitet am 6. April 2015 um 21:47 Uhr durch Mps (Diskussion | Beiträge) (AZ: Die Seite wurde neu angelegt: <source lang="csharp" style="overflow:auto;"> using System; using System.Collections.Generic; using Sy…). Sie kann sich erheblich von der aktuellen Version unterscheiden.
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;

namespace AnimeListenUpdater
{
    class AnimeListenUpdaterException : Exception
    {
        public AnimeListenUpdaterException(String message)
            : base(message)
        { }
    }
    class MediawikiException : AnimeListenUpdaterException
    {
        public MediawikiException(String message)
            : base(message)
        { }

        public MediawikiException(String code, String info)
            : base("Mediawiki error \"" + code + ": " + info + "\"")
        { }
    }

    public class Mediawiki
    {
        protected String language;
        static CookieContainer cookieContainer = new CookieContainer();
        static readonly String userAgentString = GetUserAgentString();
        protected bool loggedIn = false;

        public Mediawiki(String lang)
        {
            // "100-continue"-Mechanimus nach RFC 2616 deaktivieren, da von einigen Servern nicht unterstützt (Fehler "417 Expectation Failed.")
            System.Net.ServicePointManager.Expect100Continue = false;

            this.language = lang;
        }

        static String GetUserAgentString()
        {
            AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();

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

            return userAgent;
        }

        // Limitierung: Parameterwert darf nicht länger als 32766 Bytes sein
        private static String EncodeQuery(IEnumerable<KeyValuePair<String, object>> query)
        {
            return query
                .Select(x => { if (x.Value == null) return x.Key; else return x.Key + "=" + Uri.EscapeDataString(x.Value.ToString()); })
                .Aggregate((x, y) => x + "&" + y);
        }

        private static String MultipartQuery(IEnumerable<KeyValuePair<String, object>> query, String boundaryIdentifier)
        {
            StringBuilder sb = new StringBuilder();
            foreach (KeyValuePair<String, object> kvp in query)
            {
                sb.Append("--");
                sb.Append(boundaryIdentifier);
                sb.Append("\r\nContent-Disposition: form-data; name=\"");
                sb.Append(kvp.Key);
                sb.Append("\"\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n");
                sb.Append(kvp.Value);
                sb.Append("\r\n");
            }
            return sb.ToString();
        }

        Dictionary<String, object> GetDefaultParameters()
        {
            Dictionary<String, object> result = new Dictionary<String, object>() { { "format", "xml" } };
            if (loggedIn) result.Add("assert", "user");
            return result;
        }

        HttpWebRequest GenerateHttpWebRequest(Dictionary<String, object> query)
        {
            String url = "https://" + language + ".wikipedia.org/w/api.php";
            if (query != null && query.Count > 0)
            {
                url += "?" + EncodeQuery(GetDefaultParameters());
                url = String.Join("&", url, EncodeQuery(query));
            }

            HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
            request.UserAgent = userAgentString;
            request.CookieContainer = cookieContainer;
            request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
            request.KeepAlive = true;
            return request;
        }

        public HttpWebResponse HttpGet(Dictionary<String, object> query)
        {
            HttpWebRequest request = GenerateHttpWebRequest(query);
            return request.GetResponse() as HttpWebResponse;
        }

        public HttpWebResponse HttpPost(Dictionary<String, object> data)
        {
            HttpWebRequest request = GenerateHttpWebRequest(null);
            Dictionary<String, object> postData = new Dictionary<string, object>(GetDefaultParameters());
            foreach (String key in data.Keys) postData.Add(key, data[key]);
            request.Method = "POST";
            String boundary = Guid.NewGuid().ToString();
            byte[] postContent = new UTF8Encoding(false).GetBytes(MultipartQuery(postData, boundary));
            request.ContentType = "multipart/form-data; boundary=" + boundary;
            request.ContentLength = postContent.Length;
            
            using (Stream stream = request.GetRequestStream()) stream.Write(postContent, 0, postContent.Length);

            return request.GetResponse() as HttpWebResponse;
        }

        public static void TraverseHttpWebResponse(HttpWebResponse response, Action<XmlReader> onXmlNode)
        {
            using (response)
            using (Stream stream = response.GetResponseStream())
            using (XmlReader 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)
        {
            System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
            byte[] md5Hash = md5.ComputeHash(data);
            return BitConverter.ToString(md5Hash).Replace("-", "").ToLowerInvariant();
        }

        public static void CheckForError(XmlReader reader)
        {
            if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "error"))
                throw new MediawikiException(reader.GetAttribute("code"), reader.GetAttribute("info"));
        }

        public void Login(String username, String password, String token = null)
        {
            Dictionary<String, object> post = new Dictionary<String, object>() {
                { "action", "login" },
                { "lgname", username },
                { "lgpassword", password }
            };
            if (token != null) post.Add("lgtoken", token);
            Boolean retryWithToken = false;

            TraverseHttpWebResponse(HttpPost(post), delegate(XmlReader reader)
            {
                if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "login"))
                {
                    String result = reader.GetAttribute("result");
                    if (result == "NeedToken")
                    {
                        token = reader.GetAttribute("token");
                        retryWithToken = true;
                    }
                    else if (result == "Success") { loggedIn = true; }
                    else throw new MediawikiException(result);
                }
            });

            if (retryWithToken) Login(username, password, token);
        }

        public void Logout()
        {
            using (HttpWebResponse response = HttpGet(new Dictionary<String, object> { { "action", "logout" } })) { }
            loggedIn = false;
        }

        public String GetEditToken(String pageTitle = null)
        {
            String token = null;
            TraverseHttpWebResponse(HttpGet(new Dictionary<String, object> { { "action", "query" }, { "meta", "tokens" }, { "type", "csrf" } }), delegate(XmlReader reader)
            {
                if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "tokens"))
                {
                    token = reader.GetAttribute("csrftoken");
                }
            });
            return token;
        }
    }

    public class Program
    {
        static Mediawiki wiki;
        static String editToken = null;

        public class AnimeItem
        {
            public String List;
            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;

            public static AnimeItem Parse(Dictionary<String, String> parsedTemplate, String list)
            {
                AnimeItem result = new AnimeItem();
                try
                {
                    result.List = list;
                    result.Level = Int32.Parse(parsedTemplate["1"]);
                    result.Year = Int32.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;
                        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()
            {
                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();
            }

            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)
                    {
                        curIdx = split[i].IndexOf("[[", curIdx);
                        if (curIdx >= 0)
                        {
                            doubleBracketCount++;
                            curIdx += 2;
                        }
                    }
                    curIdx = 0;
                    while (curIdx >= 0)
                    {
                        curIdx = split[i].IndexOf("]]", curIdx);
                        if (curIdx >= 0)
                        {
                            doubleBracketCount--;
                            curIdx += 2;
                        }
                    }
                }

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

        // zur Sortierung alle Zeichen außer Buchstaben, Zahlen und Leerzeichen entfernen und Buchstaben in Kleinschreibung umwandeln
        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();
        }

        static void WriteContent(String pagetitle, String content, String basetimestamp = null)
        {
            if (editToken == null) editToken = wiki.GetEditToken();

            try
            {
                System.Console.Write("Schreibe " + pagetitle);
                String md5Hash = Mediawiki.CalcMd5Hash(new UTF8Encoding(false).GetBytes(content));

                Dictionary<String, object> post = new Dictionary<String, object>()
                {
                            { "action", "edit" }, 
                            { "title", pagetitle }, 
                            { "notminor", null },
                            { "nocreate", null },
                            { "summary", "Listen-Aktualisierung per [[Benutzer:Mps/AnimeListenUpdater.cs]]" }, 
                            { "md5", md5Hash },
                            { "text", content }, 
                            { "token", editToken }
                };
                if (basetimestamp != null) post.Add("basetimestamp", basetimestamp);

                using (HttpWebResponse response = wiki.HttpPost(post))
                using (Stream stream = response.GetResponseStream())
                using (XmlReader reader = XmlReader.Create(stream))
                {
                    while (reader.Read()) Mediawiki.CheckForError(reader);
                }
                System.Console.WriteLine(".");
            }
            catch (Exception e)
            {
                System.Console.Error.WriteLine(pagetitle + ": " + e.Message);
            }
        }

        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();

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

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

            return hasAtLeastOneYear;
        }

        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 (String curStudio in linkedStudios)
            {
                writer.WriteLine("{{#ifeq: {{{1}}}|" + curStudio + "|");

                Dictionary<String, List<AnimeItem>> 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
                bool hasCaptions = false;
                foreach (String 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("{{(!}} class=\"toptextcells\"");
                        writer.WriteLine("{{!}}");

                        int newColIdx = (int)Math.Ceiling(animesOfStudio[animeType].Count / 2.0);
                        int animeIdx = 0;
                        foreach (AnimeItem anime in animesOfStudio[animeType])
                        {
                            writer.WriteLine("* " + anime.Year + ": ''" + anime.GetWikilink() + "''");
                            animeIdx++;
                            if (animeIdx == newColIdx) writer.WriteLine("{{!}}");
                        }

                        writer.WriteLine("{{!)}}");

                        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 ===");
                    int newColIdx = (int)Math.Ceiling(otherAnimes.Count() / 2.0);
                    if (hasCaptions || newColIdx >= 5)
                    {
                        writer.WriteLine("{{(!}} class=\"toptextcells\"");
                        writer.WriteLine("{{!}}");
                    }
                    else newColIdx = -1;

                    int animeIdx = 0;
                    foreach (AnimeItem anime in otherAnimes)
                    {
                        String desc;
                        if (animeTypes.ContainsKey(anime.Type)) desc = " (" + animeTypes[anime.Type] + ")"; else desc = "";
                        writer.WriteLine("* " + anime.Year + ": ''" + anime.GetWikilink() + "''" + desc);
                        animeIdx++;
                        if (animeIdx == newColIdx) writer.WriteLine("{{!}}");
                    }

                    if (hasCaptions || newColIdx >= 5) writer.WriteLine("{{!)}}");
                }
                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;
        }

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

            var linkedStudios = allStudios
                .Where(studio => studio.Name.StartsWith("[["))
                .Select(studio => new { Name = AnimeItem.GetWikiArticle(studio.Name), List = 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 { Name = studio.Name, List = 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) });

            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");

            Dictionary<String, List<String>> combinedStudioList = allStudios
                .Where(studio => !String.IsNullOrEmpty(studio.Name))
                .Select(studio => new { Name = AnimeItem.GetWikiArticle(studio.Name), List = 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");
            String[] allStudiosArray = combinedStudioList.Keys.ToArray();
            for (int i = 0; i < allStudiosArray.Length; i++)
            {
                String targetStudio = allStudiosArray[i];
                Dictionary<String, int> similarStudios = new Dictionary<String, int>();
                for (int j = 0/*i + 1*/; j < allStudiosArray.Length; j++)
                {
                    if (i == j) continue;

                    String otherStudio = allStudiosArray[j];

                    int 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 (String 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;
        }

        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;

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

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

            for (int j = 1; j <= target.Length; j++)
            {
                for (int 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 int[] {
                            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];
        }

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

        static void Main(string[] args)
        {
            String user = null;
            String password = null;
            String matchStudiosFilename = null;
            #region Kommandozeilenparameter auswerten
            for (int i = 0; i < args.Length; i++)
            {
                if (args[i] == "-?")
                {
                    System.Console.WriteLine(Assembly.GetExecutingAssembly().GetName() + " [Parameter]");
                    System.Console.WriteLine();
                    System.Console.WriteLine("-user Benutzername      Benutzername angeben");
                    System.Console.WriteLine("-pass Passwort          Passwort angeben");
                    System.Console.WriteLine("-matchstudios Datei     Studionamen in den Jahreslisten angleichen.");
                    System.Console.WriteLine("                        Eingabe ist eine UTF8-kodierte Datei die zeilenweise die Ersetzungen angibt im Format: Ist<TAB>Soll");
                    return;
                } else
                    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))
                                {
                                    System.Console.WriteLine("Datei nicht gefunden!");
                                    return;
                                }
                                break;
                        }
                    }
            }
            #endregion

            #region Nutzername und Passwort für Edit
            if (user == null)
            {
                System.Console.Write("Benutzer: ");
                user = System.Console.ReadLine();
            }
            if (password == null)
            {
                System.Console.Write("Passwort: ");
                password = "";
                // Sternchen zeigen, statt eingetipptem Passwort
                while (true)
                {
                    ConsoleKeyInfo keyInfo = System.Console.ReadKey(true);
                    if (keyInfo.Key == ConsoleKey.Enter)
                    {
                        System.Console.WriteLine();
                        break;
                    }
                    else
                        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
                                System.Console.Write(keyInfo.KeyChar + " " + keyInfo.KeyChar);
                            }
                        }
                        else
                        {
                            password += keyInfo.KeyChar;
                            System.Console.Write("*");
                        }
                }
                System.Console.WriteLine();
            }
            #endregion

            wiki = new Mediawiki("de");
            try
            {
                wiki.Login(user, password);
                #region Eingabelisten lesen
                System.Console.WriteLine("Alphabetische Listen lesen:");
                System.Console.Write("  ");
                Dictionary<String, ContentAndTimestamp> alphabeticLists = new Dictionary<String, ContentAndTimestamp>();
                Parallel.ForEach(alphaList, letter =>
                {
                    Mediawiki.TraverseHttpWebResponse(wiki.HttpGet(new Dictionary<String, object> {
                    { "action", "query" },
                    { "prop", "revisions" },
                    { "titles", "Liste der Anime-Titel/" + letter },
                    { "rvprop", "content|timestamp" } }),
                    delegate(XmlReader reader)
                    {
                        if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "rev"))
                        {
                            String curTimestamp = reader.GetAttribute("timestamp");
                            String content = reader.ReadString();
                            lock (alphabeticLists)
                            {
                                alphabeticLists.Add(letter, new ContentAndTimestamp() { Content = content, Timestamp = curTimestamp });
                                System.Console.Write(letter);
                            }
                        }
                    });
                });
                System.Console.WriteLine();
                #endregion

                if (matchStudiosFilename != null)
                {
                    #region Studionamen ersetzen
                    System.Console.WriteLine("\nErsetzungen:");
                    Dictionary<Regex, String> replacers = new Dictionary<Regex, String>();
                    foreach (String line in File.ReadAllLines(matchStudiosFilename, Encoding.UTF8))
                    {
                        String[] 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]);
                        System.Console.WriteLine("  " + items[0] + " -> " + items[1]);
                    }
                    System.Console.Write("Fortfahren [j/n]: ");
                    if (System.Console.ReadLine().ToLowerInvariant() == "j")
                    {
                        foreach (String letter in alphabeticLists.Keys.OrderBy(key => key))
                        {
                            String content = alphabeticLists[letter].Content;
                            foreach (Regex 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
                    List<AnimeItem> animeList = new List<AnimeItem>();
                    Regex templateRegex = new Regex(@"\{\{\s*Anime\-Listeneintrag.+?}}", RegexOptions.Singleline);
                    Regex commentRegex = new Regex(@"<!--.+?-->", RegexOptions.Singleline);
                    foreach (String letter in alphabeticLists.Keys.OrderBy(key => key))
                    {
                        System.Console.WriteLine("Liste " + letter);
                        String content = commentRegex.Replace(alphabeticLists[letter].Content, "");
                        foreach (Match match in templateRegex.Matches(content))
                        {
                            Dictionary<String, String> entry = ParseTemplate(match.Value);
                            AnimeItem anime = AnimeItem.Parse(entry, letter);
                            if (anime != null)
                            {
                                animeList.Add(anime);
                                //System.Console.WriteLine("  " + anime);
                            }
                            else System.Console.Error.WriteLine("Nicht auswertbarer Eintrag: " + match.Value);
                        }
                    }
                    #endregion

                    // Jahreslisten ergänzen
                    int maxYear = animeList.Select(anime => anime.Year).Max();
                    if (maxYear > DateTime.Now.Year + 1)
                    {
                        System.Console.Write("Das höchste gefundene Jahr ist " + maxYear + ". Jahreslisten bis dahin erstellen [j/n]: ");
                        if (System.Console.ReadLine().ToLowerInvariant() != "j") return;
                    }
                    for (int year = 2000; year <= maxYear; year = year + 2)
                    {
                        yearList.Add(year);
                        yearList.Add(year + 1);
                    }
                    for (int yearIdx = 0; yearIdx < yearList.Count / 2; yearIdx++)
                    {
                        int startYear = yearList[yearIdx * 2];
                        int 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());
                }
                System.Console.WriteLine("Fertig.");
            }
            catch (MediawikiException ex)
            {
                System.Console.Error.WriteLine("Mediawiki-Fehler: " + ex.Message);
            }
            finally
            {
                //wiki.Logout();
            }
#if (DEBUG)
            Console.ReadLine();
#endif
        }
    }
}