{"action":"created","comment":{"url":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/comments/82606922","pull_request_review_id":3498535,"id":82606922,"diff_hunk":"@@ -0,0 +1,594 @@\n+﻿using System;\r\n+using System.Collections.Generic;\r\n+using System.Diagnostics;\r\n+using System.Linq;\r\n+using System.Text.RegularExpressions;\r\n+using JMMServer.Repositories.NHibernate;\r\n+using NHibernate;\r\n+\r\n+namespace JMMServer.Tasks\r\n+{\r\n+    /// <remarks>\r\n+    /// This class is NOT thread safe.\r\n+    /// </remarks>\r\n+    internal class AutoAnimeGroupCalculator\r\n+    {\r\n+        private static readonly Regex TitleNoiseRegex = new Regex( @\"[^\\w\\s]|\\d|gekijouban|the animation|the movie\",\r\n+            RegexOptions.IgnoreCase | RegexOptions.Compiled);\r\n+        private static readonly char[] Whitespace = { ' ', '\\t', '\\r', '\\n' };\r\n+\r\n+        private readonly ILookup<int, AnimeRelation> _relationMap;\r\n+        private readonly Dictionary<int, int> _animeGroupMap = new Dictionary<int, int>();\r\n+        private readonly AutoGroupExclude _exclusions;\r\n+\r\n+        /// <summary>\r\n+        /// Initializes a new <see cref=\"AutoAnimeGroupCalculator\"/> instance.\r\n+        /// </summary>\r\n+        /// <param name=\"relationMap\">A <see cref=\"ILookup{TKey,TElement}\"/> that maps anime IDs to their relations.</param>\r\n+        /// <param name=\"exclusions\">The relation/anime types to ignore when building relation graphs.</param>\r\n+        /// <exception cref=\"ArgumentNullException\"><paramref name=\"relationMap\"/> is <c>null</c>.</exception>\r\n+        public AutoAnimeGroupCalculator(ILookup<int, AnimeRelation> relationMap,\r\n+            AutoGroupExclude exclusions = AutoGroupExclude.SameSetting | AutoGroupExclude.Character)\r\n+        {\r\n+            if (relationMap == null)\r\n+                throw new ArgumentNullException(nameof(relationMap));\r\n+\r\n+            _relationMap = relationMap;\r\n+            _exclusions = exclusions;\r\n+        }\r\n+\r\n+        /// <summary>\r\n+        /// Creates a new <see cref=\"AutoAnimeGroupCalculator\"/> using relationships stored in the database.\r\n+        /// </summary>\r\n+        /// <param name=\"session\">The NHibernate session.</param>\r\n+        /// <returns>The created <see cref=\"AutoAnimeGroupCalculator\"/>.</returns>\r\n+        /// <exception cref=\"ArgumentNullException\"><paramref name=\"session\"/> is <c>null</c>.</exception>\r\n+        public static AutoAnimeGroupCalculator Create(ISessionWrapper session)\r\n+        {\r\n+            AutoGroupExclude exclusions = AutoGroupExclude.None;\r\n+            string exclusionsSetting = ServerSettings.AutoGroupSeriesRelationExclusions;\r\n+\r\n+            if (!String.IsNullOrEmpty(exclusionsSetting))\r\n+            {\r\n+                exclusions = exclusionsSetting.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)\r\n+                    .Select(s =>\r\n+                        {\r\n+                            AutoGroupExclude exclude;\r\n+\r\n+                            s = s.Replace(\" \", String.Empty);\r\n+                            Enum.TryParse(s, true, out exclude);\r\n+\r\n+                            return exclude;\r\n+                        })\r\n+                    .Aggregate(AutoGroupExclude.None, (exclude, allExcludes) => allExcludes | exclude);\r\n+            }\r\n+\r\n+            return Create(session, exclusions);\r\n+        }\r\n+\r\n+        /// <summary>\r\n+        /// Creates a new <see cref=\"AutoAnimeGroupCalculator\"/> using relationships stored in the database.\r\n+        /// </summary>\r\n+        /// <param name=\"session\">The NHibernate session.</param>\r\n+        /// <param name=\"exclusions\">The relation/anime types to ignore when building relation graphs.</param>\r\n+        /// <returns>The created <see cref=\"AutoAnimeGroupCalculator\"/>.</returns>\r\n+        /// <exception cref=\"ArgumentNullException\"><paramref name=\"session\"/> is <c>null</c>.</exception>\r\n+        public static AutoAnimeGroupCalculator Create(ISessionWrapper session, AutoGroupExclude exclusions)\r\n+        {\r\n+            if (session == null)\r\n+                throw new ArgumentNullException(nameof(session));\r\n+\r\n+            var relationshipMap = session.CreateSQLQuery(@\"\r\n+                SELECT    fromAnime.AnimeID AS fromAnimeId\r\n+                        , toAnime.AnimeID AS toAnimeId\r\n+                        , fromAnime.AnimeType AS fromAnimeType\r\n+                        , toAnime.AnimeType AS toAnimeType\r\n+                        , fromAnime.MainTitle AS fromMainTitle\r\n+                        , toAnime.MainTitle AS toMainTitle\r\n+                        , rel.RelationType AS relationType\r\n+                    FROM AniDB_Anime_Relation rel\r\n+                        INNER JOIN AniDB_Anime fromAnime\r\n+                            ON fromAnime.AnimeID = rel.AnimeID\r\n+                        INNER JOIN AniDB_Anime toAnime\r\n+                            ON toAnime.AnimeID = rel.RelatedAnimeID\")\r\n+                .AddScalar(\"fromAnimeId\", NHibernateUtil.Int32)\r\n+                .AddScalar(\"toAnimeId\", NHibernateUtil.Int32)\r\n+                .AddScalar(\"fromAnimeType\", NHibernateUtil.Int32)\r\n+                .AddScalar(\"toAnimeType\", NHibernateUtil.Int32)\r\n+                .AddScalar(\"fromMainTitle\", NHibernateUtil.String)\r\n+                .AddScalar(\"toMainTitle\", NHibernateUtil.String)\r\n+                .AddScalar(\"relationType\", NHibernateUtil.String)\r\n+                .List<object[]>()\r\n+                .Select(r =>\r\n+                    {\r\n+                        var relation = new AnimeRelation\r\n+                            {\r\n+                                FromId = (int)r[0],\r\n+                                ToId = (int)r[1],\r\n+                                FromType = (AnimeTypes)r[2],\r\n+                                ToType = (AnimeTypes)r[3],\r\n+                                FromMainTitle = (string)r[4],\r\n+                                ToMainTitle = (string)r[5],\r\n+                            };\r\n+\r\n+                        switch (((string)r[6]).ToLowerInvariant())\r\n+                        {\r\n+                            case \"full story\":\r\n+                                relation.RelationType = AnimeRelationType.FullStory;\r\n+                                break;\r\n+                            case \"summary\":\r\n+                                relation.RelationType = AnimeRelationType.Summary;\r\n+                                break;\r\n+                            case \"parent story\":\r\n+                                relation.RelationType = AnimeRelationType.ParentStory;\r\n+                                break;\r\n+                            case \"side story\":\r\n+                                relation.RelationType = AnimeRelationType.SideStory;\r\n+                                break;\r\n+                            case \"prequel\":\r\n+                                relation.RelationType = AnimeRelationType.Prequel;\r\n+                                break;\r\n+                            case \"sequel\":\r\n+                                relation.RelationType = AnimeRelationType.Sequel;\r\n+                                break;\r\n+                            case \"alternative setting\":\r\n+                                relation.RelationType = AnimeRelationType.AlternativeSetting;\r\n+                                break;\r\n+                            case \"alternative version\":\r\n+                                relation.RelationType = AnimeRelationType.AlternativeVersion;\r\n+                                break;\r\n+                            case \"same setting\":\r\n+                                relation.RelationType = AnimeRelationType.SameSetting;\r\n+                                break;\r\n+                            case \"character\":\r\n+                                relation.RelationType = AnimeRelationType.Character;\r\n+                                break;\r\n+                            default:\r\n+                                relation.RelationType = AnimeRelationType.Other;\r\n+                                break;\r\n+                        }\r\n+\r\n+                        return relation;\r\n+                    })\r\n+                .ToLookup(k => k.FromId);\r\n+\r\n+            return new AutoAnimeGroupCalculator(relationshipMap, exclusions);\r\n+        }\r\n+\r\n+        /// <summary>\r\n+        /// Gets the ID of the anime represents the group containing the specified <see cref=\"animeId\"/>.\r\n+        /// </summary>\r\n+        /// <param name=\"animeId\">The ID of the anime to get the group's anime ID for.</param>\r\n+        /// <returns>The group's representative anime ID. For anime that don't have any suitable relations,\r\n+        /// this will just be <paramref name=\"animeId\"/>.</returns>\r\n+        public int GetGroupAnimeId(int animeId)\r\n+        {\r\n+            int mainAnimeId;\r\n+\r\n+            // Check to see if the group for the specified anime has been previously calculated\r\n+            if (_animeGroupMap.TryGetValue(animeId, out mainAnimeId))\r\n+            {\r\n+                return mainAnimeId;\r\n+            }\r\n+\r\n+            Dictionary<int, RelationNode> nodes;\r\n+            HashSet<RelationEdge> edges;\r\n+\r\n+            if (BuildGroupGraph(animeId, out nodes, out edges))\r\n+            {\r\n+                mainAnimeId = FindSuitableAnimeForGroup(nodes, edges);\r\n+\r\n+                // Remember main anime ID for all anime in the relation graph\r\n+                foreach (int id in nodes.Keys)\r\n+                {\r\n+                    _animeGroupMap[id] = mainAnimeId;\r\n+                }\r\n+            }\r\n+            else // Specified anime doesn't appear to have any relations\r\n+            {\r\n+                _animeGroupMap[animeId] = mainAnimeId = animeId;\r\n+            }\r\n+\r\n+            return mainAnimeId;\r\n+        }\r\n+\r\n+        private static int FindSuitableAnimeForGroup(Dictionary<int, RelationNode> nodes, HashSet<RelationEdge> edges)\r\n+        {\r\n+            var animeRelStats = new List<AnimeRelationStats>(nodes.Count);\r\n+\r\n+            foreach (RelationNode node in nodes.Values)\r\n+            {\r\n+                var stats = new AnimeRelationStats { AnimeNode = node };\r\n+                var visitedNodes = new HashSet<int>();\r\n+                var toVisit = new Queue<int>();\r\n+\r\n+                toVisit.Enqueue(node.AnimeId);\r\n+\r\n+                // Calculate the number of sequels this anime has had by traversing the prequel/sequel relationships\r\n+                while (toVisit.Count > 0)\r\n+                {\r\n+                    int animeId = toVisit.Dequeue();\r\n+\r\n+                    if (visitedNodes.Contains(animeId))\r\n+                    {\r\n+                        continue;\r\n+                    }\r\n+\r\n+                    bool hasSequel = false;\r\n+\r\n+                    foreach (RelationEdge edge in edges.Where(e => e.AnimeId2 == animeId && e.RelationType1 == AnimeRelationType.Prequel))\r\n+                    {\r\n+                        toVisit.Enqueue(edge.AnimeId1);\r\n+                        hasSequel = true;\r\n+                    }\r\n+\r\n+                    if (hasSequel)\r\n+                    {\r\n+                        stats.Sequels++;\r\n+                    }\r\n+\r\n+                    visitedNodes.Add(animeId);\r\n+                }\r\n+\r\n+                // Count the number of various sorts of direct relationships this node has\r\n+                foreach (RelationEdge edge in edges)\r\n+                {\r\n+                    if ((edge.AnimeId1 == node.AnimeId || edge.AnimeId2 == node.AnimeId) && edge.RelationType1 == AnimeRelationType.AlternativeSetting)\r\n+                    {\r\n+                        stats.AlternateVersions++;\r\n+                    }\r\n+                    if (edge.AnimeId2 == node.AnimeId && edge.RelationType1 == AnimeRelationType.FullStory)\r\n+                    {\r\n+                        stats.FullStory++;\r\n+                    }\r\n+                    if (edge.AnimeId2 == node.AnimeId && edge.RelationType1 == AnimeRelationType.ParentStory)\r\n+                    {\r\n+                        stats.ParentStory++;\r\n+                    }\r\n+                }\r\n+\r\n+                animeRelStats.Add(stats);\r\n+            }\r\n+\r\n+            // Now that all stats have been collected. Choose the \"main\" anime for this group.\r\n+            // We give highest priority to whichever anime has the highest score.\r\n+            // If two or more anime has the same score, then we choose the one that was added to aniDB first\r\n+            int mainAnimeId = animeRelStats\r\n+                .OrderByDescending(s => s.Score)\r\n+                .ThenBy(s => s.AnimeNode.AnimeId)\r\n+                .Select(s => s.AnimeNode.AnimeId)\r\n+                .First();\r\n+\r\n+            return mainAnimeId;\r\n+        }\r\n+\r\n+        /// <summary>\r\n+        /// Builds a normalized relation graph of the anime directly/indirectly realted to the specified anime ID.\r\n+        /// </summary>\r\n+        /// <param name=\"rootAnimeId\">The ID of the anime we're building the graph from.</param>\r\n+        /// <param name=\"nodes\">Returns the graph nodes (Keyed by anime ID).</param>\r\n+        /// <param name=\"edges\">Returns the graph edges.</param>\r\n+        /// <returns><c>true</c> if a graph was built; otherwise, <c>false</c>\r\n+        /// (e.g. if the specified anime has no relations).</returns>\r\n+        private bool BuildGroupGraph(int rootAnimeId, out Dictionary<int, RelationNode> nodes, out HashSet<RelationEdge> edges)\r\n+        {\r\n+            var toVisit = new Queue<int>();\r\n+            bool first = true;\r\n+\r\n+            edges = null;\r\n+            nodes = null;\r\n+\r\n+            toVisit.Enqueue(rootAnimeId);\r\n+\r\n+            // Traverse the relation graph to build node and edge sets\r\n+            while (toVisit.Count > 0)\r\n+            {\r\n+                int animeId = toVisit.Dequeue();\r\n+\r\n+                foreach (AnimeRelation relation in _relationMap[animeId].Where(ShouldConsiderAnimeRelation))\r\n+                {\r\n+                    // For the very first relationship we encounter we need to add a relation node for\r\n+                    // the anime it is from (i.e. the \"anchor\" anime we're using as our starting point for graph traversal)\r\n+                    if (first)\r\n+                    {\r\n+                        var startNode = new RelationNode(relation.FromId, relation.FromType);\r\n+\r\n+                        edges = new HashSet<RelationEdge>();\r\n+                        nodes = new Dictionary<int, RelationNode>\r\n+                            {\r\n+                                [relation.FromId] = startNode\r\n+                            };\r\n+                        first = false;\r\n+                    }\r\n+\r\n+                    var relEdge = new RelationEdge(relation.RelationType, relation.FromId, relation.ToId);\r\n+\r\n+                    edges.Add(relEdge);\r\n+\r\n+                    if (nodes.ContainsKey(relation.ToId))\r\n+                    {\r\n+                        continue; // We've already visited this anime\r\n+                    }\r\n+\r\n+                    var node = new RelationNode(relation.ToId, relation.ToType);\r\n+                    nodes.Add(node.AnimeId, node);\r\n+                    toVisit.Enqueue(node.AnimeId);\r\n+                }\r\n+            }\r\n+\r\n+            return nodes != null;\r\n+        }\r\n+\r\n+        /// <summary>\r\n+        /// Determines whether or not we should consider using the specified <see cref=\"AnimeRelation\"/>.\r\n+        /// </summary>\r\n+        /// <remarks>\r\n+        /// Relationships such as Prequel/Sequel, Full Story/Summary, Parent Story/Side Story, are automatically\r\n+        /// considered (unless in exclusion list). However, the other relationship types are only considered if their main titles \"fuzzily\" match.\r\n+        /// </remarks>\r\n+        /// <param name=\"rel\">The <see cref=\"AnimeRelation\"/> to test.</param>\r\n+        /// <returns><c>true</c> if the specified <see cref=\"AnimeRelation\"/> should be considered when building\r\n+        /// the group graph; otherwise, <c>false</c>.</returns>\r\n+        private bool ShouldConsiderAnimeRelation(AnimeRelation rel)\r\n+        {\r\n+            if (((int)rel.RelationType & (int)_exclusions) != 0)\r\n+            {\r\n+                return false; // The relation is in the exclusion list, so ignore it\r\n+            }\r\n+            if ((_exclusions & AutoGroupExclude.Movie) == AutoGroupExclude.Movie && (rel.FromType == AnimeTypes.Movie || rel.ToType == AnimeTypes.Movie)\r\n+                || (_exclusions & AutoGroupExclude.Ova) == AutoGroupExclude.Ova && (rel.FromType == AnimeTypes.OVA || rel.ToType == AnimeTypes.OVA))\r\n+            {\r\n+                return false;\r\n+            }\r\n+            if (rel.RelationType != AnimeRelationType.AlternativeSetting && rel.RelationType != AnimeRelationType.Other\r\n+                && rel.RelationType != AnimeRelationType.SameSetting)\r\n+            {\r\n+                return true;\r\n+            }\r\n+\r\n+            // Perform a very poor man's string metric test.\r","path":"JMMServer/Tasks/AutoAnimeGroupCalculator.cs","position":349,"original_position":349,"commit_id":"ba1f776d209cff967194730a61e9fbc3721fbab9","original_commit_id":"ba1f776d209cff967194730a61e9fbc3721fbab9","user":{"login":"da3dsoul","id":5205810,"avatar_url":"https://avatars.githubusercontent.com/u/5205810?v=3","gravatar_id":"","url":"https://api.github.com/users/da3dsoul","html_url":"https://github.com/da3dsoul","followers_url":"https://api.github.com/users/da3dsoul/followers","following_url":"https://api.github.com/users/da3dsoul/following{/other_user}","gists_url":"https://api.github.com/users/da3dsoul/gists{/gist_id}","starred_url":"https://api.github.com/users/da3dsoul/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/da3dsoul/subscriptions","organizations_url":"https://api.github.com/users/da3dsoul/orgs","repos_url":"https://api.github.com/users/da3dsoul/repos","events_url":"https://api.github.com/users/da3dsoul/events{/privacy}","received_events_url":"https://api.github.com/users/da3dsoul/received_events","type":"User","site_admin":false},"body":"Also good, but will need setting to allow lazy grouping (group everything).","created_at":"2016-10-10T13:48:44Z","updated_at":"2016-10-10T13:48:44Z","html_url":"https://github.com/japanesemediamanager/jmmserver/pull/470#discussion_r82606922","pull_request_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/470","_links":{"self":{"href":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/comments/82606922"},"html":{"href":"https://github.com/japanesemediamanager/jmmserver/pull/470#discussion_r82606922"},"pull_request":{"href":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/470"}}},"pull_request":{"url":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/470","id":88612306,"html_url":"https://github.com/japanesemediamanager/jmmserver/pull/470","diff_url":"https://github.com/japanesemediamanager/jmmserver/pull/470.diff","patch_url":"https://github.com/japanesemediamanager/jmmserver/pull/470.patch","issue_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/issues/470","number":470,"state":"closed","locked":false,"title":"Improved performance of RecreateAllGroups operation","user":{"login":"pmcleish","id":13759848,"avatar_url":"https://avatars.githubusercontent.com/u/13759848?v=3","gravatar_id":"","url":"https://api.github.com/users/pmcleish","html_url":"https://github.com/pmcleish","followers_url":"https://api.github.com/users/pmcleish/followers","following_url":"https://api.github.com/users/pmcleish/following{/other_user}","gists_url":"https://api.github.com/users/pmcleish/gists{/gist_id}","starred_url":"https://api.github.com/users/pmcleish/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pmcleish/subscriptions","organizations_url":"https://api.github.com/users/pmcleish/orgs","repos_url":"https://api.github.com/users/pmcleish/repos","events_url":"https://api.github.com/users/pmcleish/events{/privacy}","received_events_url":"https://api.github.com/users/pmcleish/received_events","type":"User","site_admin":false},"body":"- Moved RecreateAllGroups operation into it's own class.\r\n- Auto group creation based on aniDB relations moved into AutoAnimeGroupCalculator class.\r\n- Updated WCF and Nancy endpoints to use new RecreateAllGroups logic.\r\n- Modified PocoCache to allow Clearing.\r\n- BaseCachedRepository.Populate no longer key selector delegate. A SelectKey method is used instead (prevents caller needing to know what key to use).\r\n- Changed some return types/method parameters of BaseXXXRepository from List to IReadOnlyXXX instead (This was intended for a reason that I didn't get around to implementing).\r\n\r\nNOTE: This will need testing by someone that has more understanding of what the RecreateAllGroups operation is supposed to do.\r\nI've tried to replicate the logic as best as I could, but some of it I didn't quite understand or wasn't sure if it was necessary. I've also had to bypass all the standard entity logic (e.g. Saving, Deleting, etc), so I'm hoping I've covered the existing logic the best I could.\r\n\r\nThe things I've tested and seem to work:\r\n- Recreating all groups both with and without auto grouping based on aniDB relations.\r\n- Checked to make sure the relation and anime type exclusions influence auto grouping.\r\n- Changed language settings (e.g. primary language set to Kanji) and made sure the group names reflect the language choice.\r\n\r\nI've also test with both SQL Server and SQLite (haven't tested with MySQL yet).\r\n\r\nThe original code for me took **5h03m** (Not auto grouping using relations) using SQL Server (1130 series, 3713 aniDB anime).\r\nThe new code now takes around **6 to 9 seconds**. Note that the new code uses parallelism for a few things, on my machine that made it 2 to 3 times faster than without.","created_at":"2016-10-10T05:48:23Z","updated_at":"2016-10-10T13:48:44Z","closed_at":"2016-10-10T13:11:42Z","merged_at":"2016-10-10T13:11:42Z","merge_commit_sha":"b523a1877fcf741b25f22b2474a5e266629bed1f","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/470/commits","review_comments_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/470/comments","review_comment_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/comments{/number}","comments_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/issues/470/comments","statuses_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/statuses/ba1f776d209cff967194730a61e9fbc3721fbab9","head":{"label":"pmcleish:recreateallgroups_perf","ref":"recreateallgroups_perf","sha":"ba1f776d209cff967194730a61e9fbc3721fbab9","user":{"login":"pmcleish","id":13759848,"avatar_url":"https://avatars.githubusercontent.com/u/13759848?v=3","gravatar_id":"","url":"https://api.github.com/users/pmcleish","html_url":"https://github.com/pmcleish","followers_url":"https://api.github.com/users/pmcleish/followers","following_url":"https://api.github.com/users/pmcleish/following{/other_user}","gists_url":"https://api.github.com/users/pmcleish/gists{/gist_id}","starred_url":"https://api.github.com/users/pmcleish/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pmcleish/subscriptions","organizations_url":"https://api.github.com/users/pmcleish/orgs","repos_url":"https://api.github.com/users/pmcleish/repos","events_url":"https://api.github.com/users/pmcleish/events{/privacy}","received_events_url":"https://api.github.com/users/pmcleish/received_events","type":"User","site_admin":false},"repo":{"id":67094406,"name":"jmmserver","full_name":"pmcleish/jmmserver","owner":{"login":"pmcleish","id":13759848,"avatar_url":"https://avatars.githubusercontent.com/u/13759848?v=3","gravatar_id":"","url":"https://api.github.com/users/pmcleish","html_url":"https://github.com/pmcleish","followers_url":"https://api.github.com/users/pmcleish/followers","following_url":"https://api.github.com/users/pmcleish/following{/other_user}","gists_url":"https://api.github.com/users/pmcleish/gists{/gist_id}","starred_url":"https://api.github.com/users/pmcleish/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pmcleish/subscriptions","organizations_url":"https://api.github.com/users/pmcleish/orgs","repos_url":"https://api.github.com/users/pmcleish/repos","events_url":"https://api.github.com/users/pmcleish/events{/privacy}","received_events_url":"https://api.github.com/users/pmcleish/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/pmcleish/jmmserver","description":"Repository for JMM Server","fork":true,"url":"https://api.github.com/repos/pmcleish/jmmserver","forks_url":"https://api.github.com/repos/pmcleish/jmmserver/forks","keys_url":"https://api.github.com/repos/pmcleish/jmmserver/keys{/key_id}","collaborators_url":"https://api.github.com/repos/pmcleish/jmmserver/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/pmcleish/jmmserver/teams","hooks_url":"https://api.github.com/repos/pmcleish/jmmserver/hooks","issue_events_url":"https://api.github.com/repos/pmcleish/jmmserver/issues/events{/number}","events_url":"https://api.github.com/repos/pmcleish/jmmserver/events","assignees_url":"https://api.github.com/repos/pmcleish/jmmserver/assignees{/user}","branches_url":"https://api.github.com/repos/pmcleish/jmmserver/branches{/branch}","tags_url":"https://api.github.com/repos/pmcleish/jmmserver/tags","blobs_url":"https://api.github.com/repos/pmcleish/jmmserver/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/pmcleish/jmmserver/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/pmcleish/jmmserver/git/refs{/sha}","trees_url":"https://api.github.com/repos/pmcleish/jmmserver/git/trees{/sha}","statuses_url":"https://api.github.com/repos/pmcleish/jmmserver/statuses/{sha}","languages_url":"https://api.github.com/repos/pmcleish/jmmserver/languages","stargazers_url":"https://api.github.com/repos/pmcleish/jmmserver/stargazers","contributors_url":"https://api.github.com/repos/pmcleish/jmmserver/contributors","subscribers_url":"https://api.github.com/repos/pmcleish/jmmserver/subscribers","subscription_url":"https://api.github.com/repos/pmcleish/jmmserver/subscription","commits_url":"https://api.github.com/repos/pmcleish/jmmserver/commits{/sha}","git_commits_url":"https://api.github.com/repos/pmcleish/jmmserver/git/commits{/sha}","comments_url":"https://api.github.com/repos/pmcleish/jmmserver/comments{/number}","issue_comment_url":"https://api.github.com/repos/pmcleish/jmmserver/issues/comments{/number}","contents_url":"https://api.github.com/repos/pmcleish/jmmserver/contents/{+path}","compare_url":"https://api.github.com/repos/pmcleish/jmmserver/compare/{base}...{head}","merges_url":"https://api.github.com/repos/pmcleish/jmmserver/merges","archive_url":"https://api.github.com/repos/pmcleish/jmmserver/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/pmcleish/jmmserver/downloads","issues_url":"https://api.github.com/repos/pmcleish/jmmserver/issues{/number}","pulls_url":"https://api.github.com/repos/pmcleish/jmmserver/pulls{/number}","milestones_url":"https://api.github.com/repos/pmcleish/jmmserver/milestones{/number}","notifications_url":"https://api.github.com/repos/pmcleish/jmmserver/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/pmcleish/jmmserver/labels{/name}","releases_url":"https://api.github.com/repos/pmcleish/jmmserver/releases{/id}","deployments_url":"https://api.github.com/repos/pmcleish/jmmserver/deployments","created_at":"2016-09-01T03:23:11Z","updated_at":"2016-09-01T03:23:13Z","pushed_at":"2016-10-10T05:33:58Z","git_url":"git://github.com/pmcleish/jmmserver.git","ssh_url":"git@github.com:pmcleish/jmmserver.git","clone_url":"https://github.com/pmcleish/jmmserver.git","svn_url":"https://github.com/pmcleish/jmmserver","homepage":"http://jmediamanager.org/jmm-server/","size":160334,"stargazers_count":0,"watchers_count":0,"language":"C#","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"japanesemediamanager:RecreateAllGroupsRewrite","ref":"RecreateAllGroupsRewrite","sha":"0e5d79e60f85d3bf4699e659c41ea03ef7afd9a0","user":{"login":"japanesemediamanager","id":6645167,"avatar_url":"https://avatars.githubusercontent.com/u/6645167?v=3","gravatar_id":"","url":"https://api.github.com/users/japanesemediamanager","html_url":"https://github.com/japanesemediamanager","followers_url":"https://api.github.com/users/japanesemediamanager/followers","following_url":"https://api.github.com/users/japanesemediamanager/following{/other_user}","gists_url":"https://api.github.com/users/japanesemediamanager/gists{/gist_id}","starred_url":"https://api.github.com/users/japanesemediamanager/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/japanesemediamanager/subscriptions","organizations_url":"https://api.github.com/users/japanesemediamanager/orgs","repos_url":"https://api.github.com/users/japanesemediamanager/repos","events_url":"https://api.github.com/users/japanesemediamanager/events{/privacy}","received_events_url":"https://api.github.com/users/japanesemediamanager/received_events","type":"Organization","site_admin":false},"repo":{"id":22979888,"name":"jmmserver","full_name":"japanesemediamanager/jmmserver","owner":{"login":"japanesemediamanager","id":6645167,"avatar_url":"https://avatars.githubusercontent.com/u/6645167?v=3","gravatar_id":"","url":"https://api.github.com/users/japanesemediamanager","html_url":"https://github.com/japanesemediamanager","followers_url":"https://api.github.com/users/japanesemediamanager/followers","following_url":"https://api.github.com/users/japanesemediamanager/following{/other_user}","gists_url":"https://api.github.com/users/japanesemediamanager/gists{/gist_id}","starred_url":"https://api.github.com/users/japanesemediamanager/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/japanesemediamanager/subscriptions","organizations_url":"https://api.github.com/users/japanesemediamanager/orgs","repos_url":"https://api.github.com/users/japanesemediamanager/repos","events_url":"https://api.github.com/users/japanesemediamanager/events{/privacy}","received_events_url":"https://api.github.com/users/japanesemediamanager/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/japanesemediamanager/jmmserver","description":"Repository for JMM Server","fork":false,"url":"https://api.github.com/repos/japanesemediamanager/jmmserver","forks_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/forks","keys_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/keys{/key_id}","collaborators_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/teams","hooks_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/hooks","issue_events_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/issues/events{/number}","events_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/events","assignees_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/assignees{/user}","branches_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/branches{/branch}","tags_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/tags","blobs_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/git/refs{/sha}","trees_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/git/trees{/sha}","statuses_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/statuses/{sha}","languages_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/languages","stargazers_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/stargazers","contributors_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/contributors","subscribers_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/subscribers","subscription_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/subscription","commits_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/commits{/sha}","git_commits_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/git/commits{/sha}","comments_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/comments{/number}","issue_comment_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/issues/comments{/number}","contents_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/contents/{+path}","compare_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/compare/{base}...{head}","merges_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/merges","archive_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/downloads","issues_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/issues{/number}","pulls_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls{/number}","milestones_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/milestones{/number}","notifications_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/labels{/name}","releases_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/releases{/id}","deployments_url":"https://api.github.com/repos/japanesemediamanager/jmmserver/deployments","created_at":"2014-08-15T05:47:52Z","updated_at":"2016-10-02T13:19:41Z","pushed_at":"2016-10-10T13:11:44Z","git_url":"git://github.com/japanesemediamanager/jmmserver.git","ssh_url":"git@github.com:japanesemediamanager/jmmserver.git","clone_url":"https://github.com/japanesemediamanager/jmmserver.git","svn_url":"https://github.com/japanesemediamanager/jmmserver","homepage":"http://jmediamanager.org/jmm-server/","size":164489,"stargazers_count":48,"watchers_count":48,"language":"C#","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":26,"mirror_url":null,"open_issues_count":132,"forks":26,"open_issues":132,"watchers":48,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/470"},"html":{"href":"https://github.com/japanesemediamanager/jmmserver/pull/470"},"issue":{"href":"https://api.github.com/repos/japanesemediamanager/jmmserver/issues/470"},"comments":{"href":"https://api.github.com/repos/japanesemediamanager/jmmserver/issues/470/comments"},"review_comments":{"href":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/470/comments"},"review_comment":{"href":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/japanesemediamanager/jmmserver/pulls/470/commits"},"statuses":{"href":"https://api.github.com/repos/japanesemediamanager/jmmserver/statuses/ba1f776d209cff967194730a61e9fbc3721fbab9"}}}}
{"actor":{"display_login":"da3dsoul"}}