mirror of
https://github.com/VDR4Arch/vdr.git
synced 2023-10-10 13:36:52 +02:00
No longer permanently looping through PMT PIDs, which caused problems with some SatIP receivers
This commit is contained in:
parent
58e21d8e37
commit
7b1c097958
@ -2343,6 +2343,7 @@ Andr
|
||||
for reporting a bug in selecting the last replayed recording in the Recordings menu
|
||||
in case there are folders and plain recordings with names that differ only in
|
||||
non-alphanumeric characters
|
||||
for reporting a problem with permanently looping through PMT PIDs on a SatIP receiver
|
||||
|
||||
Jürgen Schilling <juergen_schilling@web.de>
|
||||
for reporting that color buttons were displayed in the recording info menu if it
|
||||
|
4
HISTORY
4
HISTORY
@ -9706,7 +9706,7 @@ Video Disk Recorder Revision History
|
||||
order to restore this functionality. However, it is recommended to use the function
|
||||
with the TimerActive parameter instead.
|
||||
|
||||
2021-05-26:
|
||||
2021-06-08:
|
||||
|
||||
- cRecordingInfo::Errors() now returns -1 for old recordings; added a missing 'const'
|
||||
(suggested by Christoph Haubrich).
|
||||
@ -9714,3 +9714,5 @@ Video Disk Recorder Revision History
|
||||
- Added missing initialization of cRecorder::lastErrors.
|
||||
- Now using __cplusplus instead of DISABLE_TEMPLATES_COLLIDING_WITH_STL, and using
|
||||
std::min(), std::max() and std::swap() if available (thanks to Winfried Köhler).
|
||||
- No longer permanently looping through PMT PIDs, which caused problems with some
|
||||
SatIP receivers (reported by André Weidemann; with help from Helmut Binder).
|
||||
|
17
device.c
17
device.c
@ -4,7 +4,7 @@
|
||||
* See the main source file 'vdr.c' for copyright information and
|
||||
* how to reach the author.
|
||||
*
|
||||
* $Id: device.c 5.2 2021/03/17 10:59:36 kls Exp $
|
||||
* $Id: device.c 5.3 2021/06/08 14:57:26 kls Exp $
|
||||
*/
|
||||
|
||||
#include "device.h"
|
||||
@ -858,6 +858,13 @@ eSetChannelResult cDevice::SetChannel(const cChannel *Channel, bool LiveView)
|
||||
cStatus::MsgChannelSwitch(this, 0, LiveView);
|
||||
|
||||
if (LiveView) {
|
||||
if (IsPrimaryDevice() && !Replaying() && !Transferring()) { // this is only for FF DVB cards!
|
||||
LOCK_CHANNELS_READ;
|
||||
if (const cChannel *ch = Channels->GetByNumber(currentChannel)) {
|
||||
if (patFilter)
|
||||
patFilter->Release(ch->Sid());
|
||||
}
|
||||
}
|
||||
StopReplay();
|
||||
DELETENULL(liveSubtitle);
|
||||
DELETENULL(dvbSubtitleConverter);
|
||||
@ -898,8 +905,6 @@ eSetChannelResult cDevice::SetChannel(const cChannel *Channel, bool LiveView)
|
||||
if (SetChannelDevice(Channel, LiveView)) {
|
||||
// Start section handling:
|
||||
if (sectionHandler) {
|
||||
if (patFilter)
|
||||
patFilter->Trigger(Channel->Sid());
|
||||
sectionHandler->SetChannel(Channel);
|
||||
sectionHandler->SetStatus(true);
|
||||
}
|
||||
@ -913,6 +918,8 @@ eSetChannelResult cDevice::SetChannel(const cChannel *Channel, bool LiveView)
|
||||
|
||||
if (Result == scrOk) {
|
||||
if (LiveView && IsPrimaryDevice()) {
|
||||
if (patFilter) // this is only for FF DVB cards!
|
||||
patFilter->Request(Channel->Sid());
|
||||
currentChannel = Channel->Number();
|
||||
// Set the available audio tracks:
|
||||
ClrAvailableTracks();
|
||||
@ -1807,6 +1814,8 @@ bool cDevice::AttachReceiver(cReceiver *Receiver)
|
||||
dsyslog("CAM %d: %sknown to decrypt channel %s (scramblingTimeout = %ds)", camSlot->MasterSlotNumber(), KnownToDecrypt ? "" : "not ", *Receiver->ChannelID().ToString(), Receiver->scramblingTimeout);
|
||||
}
|
||||
}
|
||||
if (patFilter && Receiver->ChannelID().Valid())
|
||||
patFilter->Request(Receiver->ChannelID().Sid());
|
||||
Start();
|
||||
return true;
|
||||
}
|
||||
@ -1827,6 +1836,8 @@ void cDevice::Detach(cReceiver *Receiver)
|
||||
else if (receiver[i])
|
||||
receiversLeft = true;
|
||||
}
|
||||
if (patFilter && Receiver->ChannelID().Valid())
|
||||
patFilter->Release(Receiver->ChannelID().Sid());
|
||||
mutexReceiver.Unlock();
|
||||
Receiver->device = NULL;
|
||||
Receiver->Activate(false);
|
||||
|
209
pat.c
209
pat.c
@ -4,7 +4,7 @@
|
||||
* See the main source file 'vdr.c' for copyright information and
|
||||
* how to reach the author.
|
||||
*
|
||||
* $Id: pat.c 5.1 2021/03/16 15:10:54 kls Exp $
|
||||
* $Id: pat.c 5.2 2021/06/08 14:57:26 kls Exp $
|
||||
*/
|
||||
|
||||
#include "pat.h"
|
||||
@ -285,10 +285,20 @@ int GetPmtPid(int Source, int Transponder, int ServiceId)
|
||||
class cPmtPidEntry : public cListObject {
|
||||
private:
|
||||
int pid;
|
||||
bool complete;
|
||||
int count; // the number of SIDs currently requested from this PID
|
||||
int state; // adding/deleting PIDs to/from the filter may only be done from within the Process() function,
|
||||
// otherwise there could be a deadlock between cPatFilter::mutex and cSectionHandler::mutex;
|
||||
// this member tells whether this PID needs to be added to (>0) or deleted from (<0) the filter
|
||||
bool complete; // true if all SIDs on this PID have been received
|
||||
public:
|
||||
cPmtPidEntry(int Pid);
|
||||
int Pid(void) { return pid; }
|
||||
int Count(void) { return count; }
|
||||
int State(void) { int s = state; state = 0; return s; } // returns the current state and resets it
|
||||
void SetState(void) { state = 1; }
|
||||
void ClrState(void) { state = -1; }
|
||||
void Inc(void) { if (++count == 1) state = 1; }
|
||||
void Dec(void) { if (--count == 0) state = -1; }
|
||||
int Complete(void) { return complete; }
|
||||
void SetComplete(bool State) { complete = State; }
|
||||
};
|
||||
@ -296,6 +306,8 @@ public:
|
||||
cPmtPidEntry::cPmtPidEntry(int Pid)
|
||||
{
|
||||
pid = Pid;
|
||||
count = 0;
|
||||
state = 0;
|
||||
complete = false;
|
||||
}
|
||||
|
||||
@ -309,7 +321,7 @@ private:
|
||||
int version;
|
||||
bool received;
|
||||
public:
|
||||
cPmtSidEntry(int Sid, int Pid, cPmtPidEntry *PidEntry);
|
||||
cPmtSidEntry(int Sid, cPmtPidEntry *PidEntry);
|
||||
int Sid(void) { return sid; }
|
||||
int Pid(void) { return pid; }
|
||||
cPmtPidEntry *PidEntry(void) { return pidEntry; }
|
||||
@ -319,15 +331,29 @@ public:
|
||||
void SetReceived(bool State) { received = State; }
|
||||
};
|
||||
|
||||
cPmtSidEntry::cPmtSidEntry(int Sid, int Pid, cPmtPidEntry *PidEntry)
|
||||
cPmtSidEntry::cPmtSidEntry(int Sid, cPmtPidEntry *PidEntry)
|
||||
{
|
||||
sid = Sid;
|
||||
pid = Pid;
|
||||
pid = PidEntry->Pid();
|
||||
pidEntry = PidEntry;
|
||||
version = -1;
|
||||
received = false;
|
||||
}
|
||||
|
||||
// --- cPmtSidRequest --------------------------------------------------------
|
||||
|
||||
class cPmtSidRequest : public cListObject {
|
||||
private:
|
||||
int sid;
|
||||
int count; // the number of requests for this SID
|
||||
public:
|
||||
cPmtSidRequest(int Sid) { sid = Sid; count = 1; }
|
||||
int Sid(void) { return sid; }
|
||||
int Count(void) { return count; }
|
||||
void Inc(void) { count++; }
|
||||
void Dec(void) { count--; }
|
||||
};
|
||||
|
||||
// --- cPatFilter ------------------------------------------------------------
|
||||
|
||||
//#define DEBUG_PAT_PMT
|
||||
@ -339,31 +365,94 @@ cPmtSidEntry::cPmtSidEntry(int Sid, int Pid, cPmtPidEntry *PidEntry)
|
||||
|
||||
cPatFilter::cPatFilter(void)
|
||||
{
|
||||
Trigger(0);
|
||||
patVersion = -1;
|
||||
activePmt = NULL;
|
||||
transponder = 0;
|
||||
source = 0;
|
||||
Set(0x00, 0x00); // PAT
|
||||
}
|
||||
|
||||
void cPatFilter::SetStatus(bool On)
|
||||
bool cPatFilter::TransponderChanged(void)
|
||||
{
|
||||
cMutexLock MutexLock(&mutex);
|
||||
DBGLOG("PAT filter set status %d", On);
|
||||
cFilter::SetStatus(On);
|
||||
Trigger();
|
||||
if (source != Source() || transponder != Transponder()) {
|
||||
DBGLOG("PAT filter transponder changed from %d/%d to %d/%d", source, transponder, Source(), Transponder());
|
||||
source = Source();
|
||||
transponder = Transponder();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void cPatFilter::Trigger(int Sid)
|
||||
void cPatFilter::Trigger(int)
|
||||
{
|
||||
cMutexLock MutexLock(&mutex);
|
||||
patVersion = -1;
|
||||
sectionSyncer.Reset();
|
||||
if (Sid != 0 && activePmt)
|
||||
Del(activePmt->Pid(), SI::TableIdPMT);
|
||||
activePmt = NULL;
|
||||
if (Sid >= 0) {
|
||||
sid = Sid;
|
||||
DBGLOG("PAT filter trigger SID %d", Sid);
|
||||
DBGLOG("PAT filter trigger");
|
||||
if (activePmt != pmtPidList.First()) {
|
||||
if (activePmt && activePmt->Count() == 0)
|
||||
activePmt->ClrState();
|
||||
activePmt = pmtPidList.First();
|
||||
if (activePmt && activePmt->Count() == 0) {
|
||||
activePmt->SetState();
|
||||
timer.Set(PMT_SCAN_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cPatFilter::Request(int Sid)
|
||||
{
|
||||
cMutexLock MutexLock(&mutex);
|
||||
DBGLOG("PAT filter request SID %d", Sid);
|
||||
for (cPmtSidRequest *sr = pmtSidRequestList.First(); sr; sr = pmtSidRequestList.Next(sr)) {
|
||||
if (sr->Sid() == Sid) {
|
||||
sr->Inc();
|
||||
DBGLOG("PAT filter add SID request %d (%d)", Sid, sr->Count());
|
||||
return;
|
||||
}
|
||||
}
|
||||
DBGLOG("PAT filter new SID request %d", Sid);
|
||||
pmtSidRequestList.Add(new cPmtSidRequest(Sid));
|
||||
for (cPmtSidEntry *se = pmtSidList.First(); se; se = pmtSidList.Next(se)) {
|
||||
if (se->Sid() == Sid) {
|
||||
cPmtPidEntry *pPid = se->PidEntry();
|
||||
pPid->Inc();
|
||||
DBGLOG(" PMT pid %5d SID %5d (%d)", pPid->Pid(), se->Sid(), pPid->Count());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cPatFilter::Release(int Sid)
|
||||
{
|
||||
cMutexLock MutexLock(&mutex);
|
||||
DBGLOG("PAT filter release SID %d", Sid);
|
||||
for (cPmtSidRequest *sr = pmtSidRequestList.First(); sr; sr = pmtSidRequestList.Next(sr)) {
|
||||
if (sr->Sid() == Sid) {
|
||||
sr->Dec();
|
||||
DBGLOG("PAT filter del SID request %d (%d)", Sid, sr->Count());
|
||||
if (sr->Count() == 0) {
|
||||
pmtSidRequestList.Del(sr);
|
||||
for (cPmtSidEntry *se = pmtSidList.First(); se; se = pmtSidList.Next(se)) {
|
||||
if (se->Sid() == Sid) {
|
||||
cPmtPidEntry *pPid = se->PidEntry();
|
||||
pPid->Dec();
|
||||
DBGLOG(" PMT pid %5d SID %5d (%d)", pPid->Pid(), se->Sid(), pPid->Count());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int cPatFilter::NumSidRequests(int Sid)
|
||||
{
|
||||
for (cPmtSidRequest *sr = pmtSidRequestList.First(); sr; sr = pmtSidRequestList.Next(sr)) {
|
||||
if (sr->Sid() == Sid)
|
||||
return sr->Count();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool cPatFilter::PmtPidComplete(int PmtPid)
|
||||
{
|
||||
@ -377,15 +466,16 @@ bool cPatFilter::PmtPidComplete(int PmtPid)
|
||||
void cPatFilter::PmtPidReset(int PmtPid)
|
||||
{
|
||||
for (cPmtSidEntry *se = pmtSidList.First(); se; se = pmtSidList.Next(se)) {
|
||||
if (se->Pid() == PmtPid)
|
||||
if (se->Pid() == PmtPid) {
|
||||
se->SetReceived(false);
|
||||
se->PidEntry()->SetComplete(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool cPatFilter::PmtVersionChanged(int PmtPid, int Sid, int Version, bool SetNewVersion)
|
||||
{
|
||||
int i = 0;
|
||||
for (cPmtSidEntry *se = pmtSidList.First(); se; se = pmtSidList.Next(se), i++) {
|
||||
for (cPmtSidEntry *se = pmtSidList.First(); se; se = pmtSidList.Next(se)) {
|
||||
if (se->Sid() == Sid && se->Pid() == PmtPid) {
|
||||
if (!se->Received()) {
|
||||
se->SetReceived(true);
|
||||
@ -395,7 +485,7 @@ bool cPatFilter::PmtVersionChanged(int PmtPid, int Sid, int Version, bool SetNew
|
||||
if (SetNewVersion)
|
||||
se->SetVersion(Version);
|
||||
else
|
||||
DBGLOG("PMT %d %2d %5d/%d %2d -> %2d", Transponder(), i, PmtPid, Sid, se->Version(), Version);
|
||||
DBGLOG("PMT %d %5d/%5d %2d -> %2d %d", Transponder(), PmtPid, Sid, se->Version(), Version, NumSidRequests(Sid));
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
@ -407,18 +497,39 @@ bool cPatFilter::PmtVersionChanged(int PmtPid, int Sid, int Version, bool SetNew
|
||||
void cPatFilter::SwitchToNextPmtPid(void)
|
||||
{
|
||||
if (activePmt) {
|
||||
if (activePmt->Count() == 0)
|
||||
Del(activePmt->Pid(), SI::TableIdPMT);
|
||||
if (!(activePmt = pmtPidList.Next(activePmt)))
|
||||
activePmt = pmtPidList.First();
|
||||
for (;;) {
|
||||
activePmt = pmtPidList.Next(activePmt);
|
||||
if (!activePmt || activePmt->Count() == 0)
|
||||
break;
|
||||
}
|
||||
if (activePmt) {
|
||||
PmtPidReset(activePmt->Pid());
|
||||
activePmt->SetComplete(false);
|
||||
Add(activePmt->Pid(), SI::TableIdPMT);
|
||||
timer.Set(PMT_SCAN_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cPatFilter::Process(u_short Pid, u_char Tid, const u_char *Data, int Length)
|
||||
{
|
||||
cMutexLock MutexLock(&mutex);
|
||||
if (TransponderChanged()) {
|
||||
patVersion = -1;
|
||||
sectionSyncer.Reset();
|
||||
}
|
||||
if (patVersion >= 0) {
|
||||
for (cPmtPidEntry *pPid = pmtPidList.First(); pPid; pPid = pmtPidList.Next(pPid)) {
|
||||
int State = pPid->State();
|
||||
if (State > 0)
|
||||
Add(pPid->Pid(), SI::TableIdPMT);
|
||||
else if (State < 0)
|
||||
Del(pPid->Pid(), SI::TableIdPMT);
|
||||
}
|
||||
}
|
||||
else if (Pid != 0x00)
|
||||
return;
|
||||
if (Pid == 0x00) {
|
||||
if (Tid == SI::TableIdPAT) {
|
||||
SI::PAT pat(Data, false);
|
||||
@ -426,13 +537,11 @@ void cPatFilter::Process(u_short Pid, u_char Tid, const u_char *Data, int Length
|
||||
return;
|
||||
if (sectionSyncer.Check(pat.getVersionNumber(), pat.getSectionNumber())) {
|
||||
DBGLOG("PAT %d %d -> %d %d/%d", Transponder(), patVersion, pat.getVersionNumber(), pat.getSectionNumber(), pat.getLastSectionNumber());
|
||||
bool NeedsSetStatus = patVersion >= 0;
|
||||
if (pat.getVersionNumber() != patVersion) {
|
||||
if (pat.getLastSectionNumber() > 0)
|
||||
DBGLOG(" PAT %d: %d sections", Transponder(), pat.getLastSectionNumber() + 1);
|
||||
if (activePmt) {
|
||||
Del(activePmt->Pid(), SI::TableIdPMT);
|
||||
if (NeedsSetStatus)
|
||||
SetStatus(false); // deletes all PIDs from the filter
|
||||
activePmt = NULL;
|
||||
}
|
||||
pmtSidList.Clear();
|
||||
pmtPidList.Clear();
|
||||
patVersion = pat.getVersionNumber();
|
||||
@ -441,36 +550,39 @@ void cPatFilter::Process(u_short Pid, u_char Tid, const u_char *Data, int Length
|
||||
for (SI::Loop::Iterator it; pat.associationLoop.getNext(assoc, it); ) {
|
||||
if (!assoc.isNITPid()) {
|
||||
int PmtPid = assoc.getPid();
|
||||
int PmtSid = assoc.getServiceId();
|
||||
cPmtPidEntry *pPid = NULL;
|
||||
int PidIndex = 0;
|
||||
for (pPid = pmtPidList.First(); pPid && pPid->Pid() != PmtPid; pPid = pmtPidList.Next(pPid))
|
||||
PidIndex++;
|
||||
for (pPid = pmtPidList.First(); pPid; pPid = pmtPidList.Next(pPid)) {
|
||||
if (pPid->Pid() == PmtPid)
|
||||
break;
|
||||
}
|
||||
int SidRequest = NumSidRequests(PmtSid);
|
||||
DBGLOG(" PMT pid %5d SID %5d%s%s", PmtPid, PmtSid, SidRequest ? " R" : "", pPid ? " S" : "");
|
||||
if (!pPid) { // new PMT Pid
|
||||
pPid = new cPmtPidEntry(PmtPid);
|
||||
pmtPidList.Add(pPid);
|
||||
}
|
||||
pmtSidList.Add(new cPmtSidEntry(assoc.getServiceId(), PmtPid, pPid));
|
||||
DBGLOG(" PMT pid %2d/%2d %5d SID %5d", PidIndex, pmtSidList.Count() - 1, PmtPid, assoc.getServiceId());
|
||||
if (sid == assoc.getServiceId()) {
|
||||
activePmt = pPid;
|
||||
DBGLOG("sid = %d pidIndex = %d", sid, PidIndex);
|
||||
}
|
||||
pmtSidList.Add(new cPmtSidEntry(PmtSid, pPid));
|
||||
if (SidRequest > 0)
|
||||
pPid->Inc();
|
||||
}
|
||||
}
|
||||
if (sectionSyncer.Processed(pat.getSectionNumber(), pat.getLastSectionNumber())) { // all PAT sections done
|
||||
if (pmtPidList.Count() != pmtSidList.Count())
|
||||
DBGLOG(" PAT %d: shared PMT PIDs", Transponder());
|
||||
if (pmtSidList.Count() && !activePmt)
|
||||
activePmt = pmtPidList.First();
|
||||
if (activePmt)
|
||||
Add(activePmt->Pid(), SI::TableIdPMT);
|
||||
for (cPmtPidEntry *pPid = pmtPidList.First(); pPid; pPid = pmtPidList.Next(pPid)) {
|
||||
if (pPid->Count() == 0) {
|
||||
pPid->SetState();
|
||||
activePmt = pPid;
|
||||
timer.Set(PMT_SCAN_TIMEOUT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (NeedsSetStatus)
|
||||
SetStatus(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Tid == SI::TableIdPMT && Source() && Transponder()) {
|
||||
timer.Set(PMT_SCAN_TIMEOUT);
|
||||
SI::PMT pmt(Data, false);
|
||||
if (!pmt.CheckCRCAndParse())
|
||||
return;
|
||||
@ -731,6 +843,5 @@ void cPatFilter::Process(u_short Pid, u_char Tid, const u_char *Data, int Length
|
||||
if (activePmt)
|
||||
DBGLOG("PMT timeout Pid %d", activePmt->Pid());
|
||||
SwitchToNextPmtPid();
|
||||
timer.Set(PMT_SCAN_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
14
pat.h
14
pat.h
@ -4,7 +4,7 @@
|
||||
* See the main source file 'vdr.c' for copyright information and
|
||||
* how to reach the author.
|
||||
*
|
||||
* $Id: pat.h 4.2 2020/06/19 12:19:15 kls Exp $
|
||||
* $Id: pat.h 5.1 2021/06/08 14:57:26 kls Exp $
|
||||
*/
|
||||
|
||||
#ifndef __PAT_H
|
||||
@ -16,27 +16,33 @@
|
||||
|
||||
class cPmtPidEntry;
|
||||
class cPmtSidEntry;
|
||||
class cPmtSidRequest;
|
||||
|
||||
class cPatFilter : public cFilter {
|
||||
private:
|
||||
cMutex mutex;
|
||||
cTimeMs timer;
|
||||
int patVersion;
|
||||
int sid;
|
||||
cPmtPidEntry *activePmt;
|
||||
cList<cPmtPidEntry> pmtPidList;
|
||||
cList<cPmtSidEntry> pmtSidList;
|
||||
cList<cPmtSidRequest> pmtSidRequestList;
|
||||
int source;
|
||||
int transponder;
|
||||
cSectionSyncer sectionSyncer;
|
||||
bool TransponderChanged(void);
|
||||
bool PmtPidComplete(int PmtPid);
|
||||
void PmtPidReset(int PmtPid);
|
||||
bool PmtVersionChanged(int PmtPid, int Sid, int Version, bool SetNewVersion = false);
|
||||
int NumSidRequests(int Sid);
|
||||
void SwitchToNextPmtPid(void);
|
||||
protected:
|
||||
virtual void Process(u_short Pid, u_char Tid, const u_char *Data, int Length);
|
||||
public:
|
||||
cPatFilter(void);
|
||||
virtual void SetStatus(bool On);
|
||||
void Trigger(int Sid = -1);
|
||||
void Trigger(int); // triggers reading the PMT PIDs that are currently not requested (dummy parameter for backwards compatibility, value is ignored)
|
||||
void Request(int Sid); // requests permanent reading of the PMT PID for this SID
|
||||
void Release(int Sid); // releases permanent reading of the PMT PID for this SID
|
||||
};
|
||||
|
||||
void GetCaDescriptors(int Source, int Transponder, int ServiceId, const int *CaSystemIds, cDynamicBuffer &Buffer, int EsPid);
|
||||
|
Loading…
Reference in New Issue
Block a user