mirror of
https://github.com/jellyfin/jellyfin-plugin-bookshelf.git
synced 2024-11-23 13:49:45 +00:00
Cleanup and Music Artist and Music Album support
Cleaned up some commented out code, but didn't get as much cleanup does as I'd like because I realised I should really implement music properly before removing the old code that does music. Implemened the well known Music/All Music, Music/Artist and Music/Genre folders as well as the sub items for those folders which are MusicAlbumItem, MusicArtistItem and MusicItem
This commit is contained in:
parent
a5df7eb442
commit
b21260b1d5
@ -111,7 +111,14 @@ namespace MediaBrowser.Plugins.Dlna.Model
|
||||
internal MusicContainer(User user)
|
||||
: base(user, user.RootFolder, id: "1", parentId: "0", title: "Music")
|
||||
{
|
||||
this.AllMusic = new AllMusicContainer(user);
|
||||
this.Genre = new AllMusicContainer(user);
|
||||
this.Artist = new MusicArtistContainer(user);
|
||||
}
|
||||
internal AllMusicContainer AllMusic { get; private set; }
|
||||
internal AllMusicContainer Genre { get; private set; }
|
||||
internal MusicArtistContainer Artist { get; private set; }
|
||||
|
||||
protected internal override Platinum.MediaObject MediaObject
|
||||
{
|
||||
get { return this.MediaContainer; }
|
||||
@ -120,7 +127,7 @@ namespace MediaBrowser.Plugins.Dlna.Model
|
||||
{
|
||||
get
|
||||
{
|
||||
return new List<ModelBase>();
|
||||
return new List<ModelBase>() {this.AllMusic, this.Genre, this.Artist} ;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -130,12 +137,12 @@ namespace MediaBrowser.Plugins.Dlna.Model
|
||||
: base(user, user.RootFolder, id: "2", parentId: "0", title: "Video")
|
||||
{
|
||||
this.AllVideo = new AllVideoContainer(user);
|
||||
this.Genre = new VideoFoldersContainer(user);
|
||||
this.Genre = new VideoGenreContainer(user);
|
||||
this.Actor = new ActorContainer(user);
|
||||
this.Folders = new VideoFoldersContainer(user);
|
||||
}
|
||||
internal AllVideoContainer AllVideo { get; private set; }
|
||||
internal VideoFoldersContainer Genre { get; private set; }
|
||||
internal VideoGenreContainer Genre { get; private set; }
|
||||
internal ActorContainer Actor { get; private set; }
|
||||
internal VideoFoldersContainer Folders { get; private set; }
|
||||
|
||||
@ -148,6 +155,60 @@ namespace MediaBrowser.Plugins.Dlna.Model
|
||||
}
|
||||
}
|
||||
|
||||
internal class AllMusicContainer : WellKnownContainerBase
|
||||
{
|
||||
internal AllMusicContainer(User user)
|
||||
: base(user, user.RootFolder, id: "4", parentId: "1", title: "All Music")
|
||||
{
|
||||
}
|
||||
protected internal override IEnumerable<ModelBase> Children
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.MbFolder.GetRecursiveChildren(User).OfType<MediaBrowser.Controller.Entities.Audio.Audio>().Select(i => new MusicItem(this.User, i, parentId: this.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
internal class MusicArtistContainer : WellKnownContainerBase
|
||||
{
|
||||
internal MusicArtistContainer(User user)
|
||||
: base(user, user.RootFolder, id: "6", parentId: "1", title: "Artist")
|
||||
{
|
||||
}
|
||||
protected internal override IEnumerable<ModelBase> Children
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.MbFolder.GetRecursiveChildren(this.User)
|
||||
.OfType<MediaBrowser.Controller.Entities.Audio.MusicArtist>()
|
||||
.DistinctBy(person => person.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(person => person.Name)
|
||||
.Select(i => new MusicArtistItem(this.User, i, parentId: this.Id));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
internal class MusicAlbumContainer : WellKnownContainerBase
|
||||
{
|
||||
internal MusicAlbumContainer(User user)
|
||||
: base(user, user.RootFolder, id: "7", parentId: "1", title: "Album")
|
||||
{
|
||||
}
|
||||
protected internal override IEnumerable<ModelBase> Children
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.MbFolder.GetRecursiveChildren(this.User)
|
||||
.OfType<MediaBrowser.Controller.Entities.Audio.MusicAlbum>()
|
||||
.DistinctBy(album => album.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(album => album.Name)
|
||||
.Select(album => new MusicAlbumItem(this.User, album, parentId: this.Id));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
internal class AllVideoContainer : WellKnownContainerBase
|
||||
{
|
||||
internal AllVideoContainer(User user)
|
||||
@ -162,6 +223,33 @@ namespace MediaBrowser.Plugins.Dlna.Model
|
||||
}
|
||||
}
|
||||
}
|
||||
internal class VideoGenreContainer : WellKnownContainerBase
|
||||
{
|
||||
internal VideoGenreContainer(User user)
|
||||
: base(user, user.RootFolder, id: "9", parentId: "2", title: "Genre")
|
||||
{
|
||||
}
|
||||
protected internal override IEnumerable<ModelBase> Children
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.MbFolder.GetRecursiveChildren(this.User)
|
||||
.OfType<Video>()
|
||||
.SelectMany(video =>
|
||||
{
|
||||
if (video.Genres == null)
|
||||
{
|
||||
return new string[] { };
|
||||
}
|
||||
return video.Genres.Where(genre => !string.IsNullOrWhiteSpace(genre));
|
||||
})
|
||||
.DistinctBy(genre => genre, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(genre => genre)
|
||||
.Select(genre => new VideoGenreItem(this.User, genre, parentId: this.Id));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
internal class ActorContainer : WellKnownContainerBase
|
||||
{
|
||||
internal ActorContainer(User user)
|
||||
@ -297,7 +385,60 @@ namespace MediaBrowser.Plugins.Dlna.Model
|
||||
get { return System.IO.Path.GetExtension(this.MBItem.Path); }
|
||||
}
|
||||
}
|
||||
internal class VideoGenreItem : ModelBase
|
||||
{
|
||||
internal VideoGenreItem(User user, string genre, string parentId)
|
||||
: base(user, genre.GetMD5().ToString(), parentId)
|
||||
{
|
||||
this.Genre = genre;
|
||||
}
|
||||
protected internal string Genre { get; private set; }
|
||||
|
||||
protected internal override IEnumerable<ModelBase> Children
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.User.RootFolder.GetRecursiveChildren(User)
|
||||
.OfType<Video>()
|
||||
.Where(i => i.Genres
|
||||
.Any(g => string.Equals(g, this.Genre, StringComparison.OrdinalIgnoreCase)))
|
||||
.Select(i => new VideoItem(this.User, i, parentId: this.Id));
|
||||
}
|
||||
}
|
||||
|
||||
protected internal override Platinum.MediaObject MediaObject
|
||||
{
|
||||
get { return this.MediaContainer; }
|
||||
}
|
||||
internal Platinum.MediaContainer MediaContainer
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = new Platinum.MediaContainer();
|
||||
result.ObjectID = this.Id;
|
||||
result.ParentID = this.ParentId;
|
||||
result.Class = new Platinum.ObjectClass("object.container.genre.videoGenre", "");
|
||||
|
||||
result.Title = this.Genre == null ? string.Empty : this.Genre;
|
||||
result.Description.DescriptionText = this.Genre == null ? string.Empty : this.Genre;
|
||||
result.Description.LongDescriptionText = this.Genre == null ? string.Empty : this.Genre;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
protected internal override Platinum.MediaResource MainMediaResource
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = new Platinum.MediaResource();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
protected internal override string Extension
|
||||
{
|
||||
get { return string.Empty; }
|
||||
}
|
||||
}
|
||||
internal class ActorItem : ModelBase
|
||||
{
|
||||
internal ActorItem(User user, PersonInfo item, string parentId)
|
||||
@ -352,4 +493,126 @@ namespace MediaBrowser.Plugins.Dlna.Model
|
||||
get { return string.Empty; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class MusicItem : ModelBaseItem<MediaBrowser.Controller.Entities.Audio.Audio>
|
||||
{
|
||||
internal MusicItem(User user, MediaBrowser.Controller.Entities.Audio.Audio mbItem, string parentId)
|
||||
: base(user, mbItem, parentId)
|
||||
{
|
||||
|
||||
}
|
||||
protected internal override IEnumerable<ModelBase> Children { get { return new List<ModelBase>(); } }
|
||||
protected internal override Platinum.MediaObject MediaObject
|
||||
{
|
||||
get { return this.MediaItem; }
|
||||
}
|
||||
internal Platinum.MediaItem MediaItem
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = MediaItemHelper.GetMediaItem(this.MBItem);
|
||||
result.ParentID = this.ParentId;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
protected internal override Platinum.MediaResource MainMediaResource
|
||||
{
|
||||
get
|
||||
{
|
||||
return MediaItemHelper.GetMediaResource(this.MBItem);
|
||||
}
|
||||
}
|
||||
protected internal override string Extension
|
||||
{
|
||||
get { return System.IO.Path.GetExtension(this.MBItem.Path); }
|
||||
}
|
||||
}
|
||||
|
||||
internal class MusicArtistItem : ModelBaseItem<MediaBrowser.Controller.Entities.Audio.MusicArtist>
|
||||
{
|
||||
internal MusicArtistItem(User user, MediaBrowser.Controller.Entities.Audio.MusicArtist mbItem, string parentId)
|
||||
: base(user, mbItem, parentId)
|
||||
{
|
||||
|
||||
}
|
||||
protected internal override IEnumerable<ModelBase> Children
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.User.RootFolder.GetRecursiveChildren(this.User)
|
||||
.OfType<MediaBrowser.Controller.Entities.Audio.Audio>()
|
||||
.Where(i=> string.Equals(i.Artist, this.MBItem.Name, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(i=> new MusicItem(this.User, i, this.Id));
|
||||
}
|
||||
}
|
||||
protected internal override Platinum.MediaObject MediaObject
|
||||
{
|
||||
get { return this.MediaItem; }
|
||||
}
|
||||
internal Platinum.MediaItem MediaItem
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = MediaItemHelper.GetMediaItem(this.MBItem);
|
||||
result.ParentID = this.ParentId;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
protected internal override Platinum.MediaResource MainMediaResource
|
||||
{
|
||||
get
|
||||
{
|
||||
return MediaItemHelper.GetMediaResource(this.MBItem);
|
||||
}
|
||||
}
|
||||
protected internal override string Extension
|
||||
{
|
||||
get { return System.IO.Path.GetExtension(this.MBItem.Path); }
|
||||
}
|
||||
}
|
||||
internal class MusicAlbumItem : ModelBaseItem<MediaBrowser.Controller.Entities.Audio.MusicAlbum>
|
||||
{
|
||||
internal MusicAlbumItem(User user, MediaBrowser.Controller.Entities.Audio.MusicAlbum mbItem, string parentId)
|
||||
: base(user, mbItem, parentId)
|
||||
{
|
||||
|
||||
}
|
||||
protected internal override IEnumerable<ModelBase> Children
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.User.RootFolder.GetRecursiveChildren(this.User)
|
||||
.OfType<MediaBrowser.Controller.Entities.Audio.Audio>()
|
||||
.Where(i => string.Equals(i.Album, this.MBItem.Name, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(i => new MusicItem(this.User, i, this.Id));
|
||||
}
|
||||
}
|
||||
protected internal override Platinum.MediaObject MediaObject
|
||||
{
|
||||
get { return this.MediaItem; }
|
||||
}
|
||||
internal Platinum.MediaItem MediaItem
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = MediaItemHelper.GetMediaItem(this.MBItem);
|
||||
result.ParentID = this.ParentId;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
protected internal override Platinum.MediaResource MainMediaResource
|
||||
{
|
||||
get
|
||||
{
|
||||
return MediaItemHelper.GetMediaResource(this.MBItem);
|
||||
}
|
||||
}
|
||||
protected internal override string Extension
|
||||
{
|
||||
get { return System.IO.Path.GetExtension(this.MBItem.Path); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -255,46 +255,7 @@ namespace MediaBrowser.Plugins.Dlna
|
||||
//I'm just not sure if those folders listed with object IDs are all well known across clients or if these ones are WMP specific
|
||||
//if they are device specific but also significant, then that might explain why Plex goes to the trouble of having configurable client device profiles for its DLNA server
|
||||
|
||||
//var didl = Platinum.Didl.header;
|
||||
|
||||
//IEnumerable<BaseItem> children = null;
|
||||
|
||||
//// I need to ask someone on the MB team if there's a better way to do this, it seems like it
|
||||
////could get pretty expensive to get ALL children all the time
|
||||
////if it's our only option perhaps we should cache results locally or something similar
|
||||
//children = this.CurrentUser.RootFolder.GetRecursiveChildren(this.CurrentUser);
|
||||
////children = children.Filter(Extensions.FilterType.Music | Extensions.FilterType.Video).Page(starting_index, requested_count);
|
||||
|
||||
//int itemCount = 0;
|
||||
|
||||
//if (children != null)
|
||||
//{
|
||||
// foreach (var child in children)
|
||||
// {
|
||||
|
||||
// using (var item = BaseItemToMediaItem(child, context))
|
||||
// {
|
||||
// if (item != null)
|
||||
// {
|
||||
// string test;
|
||||
// test = item.ToDidl(filter);
|
||||
// didl += item.ToDidl(filter);
|
||||
// itemCount++;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// didl += Platinum.Didl.footer;
|
||||
|
||||
// action.SetArgumentValue("Result", didl);
|
||||
// action.SetArgumentValue("NumberReturned", itemCount.ToString());
|
||||
// action.SetArgumentValue("TotalMatches", itemCount.ToString());
|
||||
|
||||
// // update ID may be wrong here, it should be the one of the container?
|
||||
// action.SetArgumentValue("UpdateId", "1");
|
||||
|
||||
// return NEP_Success;
|
||||
|
||||
|
||||
//XBOX360 Video
|
||||
//BrowseDirectChildren Entered - Parameters:
|
||||
//action: { Name:"Browse", Description:" { Name:"Browse", Arguments:[ ] } ",
|
||||
@ -323,66 +284,48 @@ namespace MediaBrowser.Plugins.Dlna
|
||||
2013-02-24 22:47:09.0699, Info, App, ProcessFileRequest Entered - Parameters: context: { LocalAddress:{ IP:192.168.1.56, Port:1733 }, RemoteAddress:{ IP:192.168.1.27, Port:9842 }, Request:"http://192.168.1.56:1733/1ce95963-d31a-3052-8cf1-f31e934bd4fe?albumArt=true", Signature:XBox } response:Platinum.HttpResponse
|
||||
2013-02-24 22:47:24.0908, Info, App, BrowseDirectChildren Entered - Parameters: action: { Name:"Browse", Description:" { Name:"Browse", Arguments:[ ] } ", Arguments:[ ] } object_id:90a8b701-b1ca-325d-e00f-d3f60267584d filter:dc:title,res,res@protection,res@duration,res@bitrate,upnp:genre,upnp:actor,res@microsoft:codec starting_index:0 requested_count:1000 sort_criteria:+upnp:class,+dc:title context: { LocalAddress:{ IP:192.168.1.56, Port:1733 }, RemoteAddress:{ IP:192.168.1.27, Port:44378 }, Request:"http://192.168.1.56:1733/ContentDirectory/944ef00a-1bd9-d8f2-02ab-9a5de207da75/control.xml", Signature:XBox }
|
||||
*/
|
||||
var didl = Platinum.Didl.header;
|
||||
int itemCount = 0;
|
||||
|
||||
IEnumerable<Model.ModelBase> children = null;
|
||||
Model.ModelBase objectIDMatch;
|
||||
// I need to ask someone on the MB team if there's a better way to do this, it seems like it
|
||||
//could get pretty expensive to get ALL children all the time
|
||||
//if it's our only option perhaps we should cache results locally or something similar
|
||||
Model.ModelBase objectIDMatch = null;
|
||||
|
||||
var root = new Model.Root(this.CurrentUser);
|
||||
if (string.Equals(object_id, "0", StringComparison.OrdinalIgnoreCase))
|
||||
objectIDMatch = root;
|
||||
else
|
||||
objectIDMatch = root.GetChildRecursive(object_id);
|
||||
|
||||
if (objectIDMatch == null)
|
||||
if (objectIDMatch != null)
|
||||
{
|
||||
didl += Platinum.Didl.footer;
|
||||
|
||||
action.SetArgumentValue("Result", didl);
|
||||
action.SetArgumentValue("NumberReturned", itemCount.ToString());
|
||||
action.SetArgumentValue("TotalMatches", itemCount.ToString());
|
||||
|
||||
// update ID may be wrong here, it should be the one of the container?
|
||||
action.SetArgumentValue("UpdateId", "1");
|
||||
|
||||
return NEP_Success;
|
||||
}
|
||||
|
||||
children = objectIDMatch.Children;
|
||||
|
||||
|
||||
if (children != null)
|
||||
{
|
||||
foreach (var child in children)
|
||||
var children = objectIDMatch.Children;
|
||||
if (children != null)
|
||||
{
|
||||
|
||||
using (var item = child.MediaObject)
|
||||
int itemCount = 0;
|
||||
var didl = Platinum.Didl.header;
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (item != null)
|
||||
using (var item = child.MediaObject)
|
||||
{
|
||||
AddContextInfo(item, child.MainMediaResource, child.Id, child.Extension, context);
|
||||
if (item != null)
|
||||
{
|
||||
AddContextInfo(item, child.MainMediaResource, child.Id, child.Extension, context);
|
||||
|
||||
string test;
|
||||
test = item.ToDidl(filter);
|
||||
didl += item.ToDidl(filter);
|
||||
itemCount++;
|
||||
string test;
|
||||
test = item.ToDidl(filter);
|
||||
didl += item.ToDidl(filter);
|
||||
itemCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
didl += Platinum.Didl.footer;
|
||||
|
||||
action.SetArgumentValue("Result", didl);
|
||||
action.SetArgumentValue("NumberReturned", itemCount.ToString());
|
||||
action.SetArgumentValue("TotalMatches", itemCount.ToString());
|
||||
|
||||
// update ID may be wrong here, it should be the one of the container?
|
||||
action.SetArgumentValue("UpdateId", "1");
|
||||
|
||||
return NEP_Success;
|
||||
}
|
||||
|
||||
didl += Platinum.Didl.footer;
|
||||
|
||||
action.SetArgumentValue("Result", didl);
|
||||
action.SetArgumentValue("NumberReturned", itemCount.ToString());
|
||||
action.SetArgumentValue("TotalMatches", itemCount.ToString());
|
||||
|
||||
// update ID may be wrong here, it should be the one of the container?
|
||||
action.SetArgumentValue("UpdateId", "1");
|
||||
|
||||
return NEP_Success;
|
||||
}
|
||||
return NEP_Failure;
|
||||
}
|
||||
@ -462,50 +405,6 @@ namespace MediaBrowser.Plugins.Dlna
|
||||
//this means it wants albums put into containers, I thought Platinum might do this for us, but it doesn't
|
||||
|
||||
|
||||
//var didl = Platinum.Didl.header;
|
||||
|
||||
//IEnumerable<BaseItem> children = null;
|
||||
|
||||
//// I need to ask someone on the MB team if there's a better way to do this, it seems like it
|
||||
////could get pretty expensive to get ALL children all the time
|
||||
////if it's our only option perhaps we should cache results locally or something similar
|
||||
//children = this.CurrentUser.RootFolder.GetRecursiveChildren(this.CurrentUser);
|
||||
////children = children.Filter(Extensions.FilterType.Music | Extensions.FilterType.Video).Page(starting_index, requested_count);
|
||||
|
||||
////var test = GetFilterFromCriteria(searchCriteria);
|
||||
//children = children.Where(GetBaseItemMatchFromCriteria(searchCriteria));
|
||||
|
||||
|
||||
//int itemCount = 0;
|
||||
|
||||
//if (children != null)
|
||||
//{
|
||||
// Platinum.MediaItem item = null;
|
||||
// foreach (var child in children)
|
||||
// {
|
||||
// item = BaseItemToMediaItem(child, context);
|
||||
|
||||
// if (item != null)
|
||||
// {
|
||||
// item.ParentID = string.Empty;
|
||||
|
||||
// didl += item.ToDidl(filter);
|
||||
// itemCount++;
|
||||
// }
|
||||
// }
|
||||
|
||||
// didl += Platinum.Didl.footer;
|
||||
|
||||
// action.SetArgumentValue("Result", didl);
|
||||
// action.SetArgumentValue("NumberReturned", itemCount.ToString());
|
||||
// action.SetArgumentValue("TotalMatches", itemCount.ToString());
|
||||
|
||||
// // update ID may be wrong here, it should be the one of the container?
|
||||
// action.SetArgumentValue("UpdateId", "1");
|
||||
|
||||
// return NEP_Success;
|
||||
//}
|
||||
//return NEP_Failure;
|
||||
var didl = Platinum.Didl.header;
|
||||
int itemCount = 0;
|
||||
|
||||
@ -663,6 +562,7 @@ namespace MediaBrowser.Plugins.Dlna
|
||||
|
||||
result.AddResource(resource);
|
||||
}
|
||||
|
||||
MediaItemHelper.AddAlbumArtInfoToMediaItem(result, id, Kernel.HttpServerUrlPrefix, ips);
|
||||
}
|
||||
}
|
||||
@ -1004,6 +904,30 @@ namespace MediaBrowser.Plugins.Dlna
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static Platinum.MediaResource GetMediaResource(MusicArtist item)
|
||||
{
|
||||
//there's nothing specific about an music artist item that requires extra Resources
|
||||
return GetMediaResource((BaseItem)item);
|
||||
}
|
||||
internal static Platinum.MediaItem GetMediaItem(MusicArtist item)
|
||||
{
|
||||
var result = GetMediaItem((BaseItem)item);
|
||||
result.Title = item.Name;
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static Platinum.MediaResource GetMediaResource(MusicAlbum item)
|
||||
{
|
||||
//there's nothing specific about an music artist item that requires extra Resources
|
||||
return GetMediaResource((BaseItem)item);
|
||||
}
|
||||
internal static Platinum.MediaItem GetMediaItem(MusicAlbum item)
|
||||
{
|
||||
var result = GetMediaItem((BaseItem)item);
|
||||
result.Title = item.Name;
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static Platinum.MediaResource GetMediaResource(BaseItem item)
|
||||
{
|
||||
var result = new Platinum.MediaResource();
|
||||
|
Loading…
Reference in New Issue
Block a user