Transcode files when copying them to devices
This commit is contained in:
parent
eae74f6ad8
commit
4b381e00fd
@ -36,6 +36,13 @@ public:
|
||||
Role_FreeSpace,
|
||||
};
|
||||
|
||||
// Values are saved in the database - don't change
|
||||
enum TranscodeMode {
|
||||
Transcode_Always = 1,
|
||||
Transcode_Never = 2,
|
||||
Transcode_Unsupported = 3,
|
||||
};
|
||||
|
||||
typedef boost::function<void (float progress)> ProgressFunction;
|
||||
|
||||
struct CopyJob {
|
||||
@ -53,6 +60,10 @@ public:
|
||||
|
||||
virtual QString LocalPath() const { return QString(); }
|
||||
|
||||
virtual TranscodeMode GetTranscodeMode() const { return Transcode_Never; }
|
||||
virtual Song::FileType GetTranscodeFormat() const { return Song::Type_Unknown; }
|
||||
virtual QList<Song::FileType> SupportedFiletypes() { return QList<Song::FileType>(); }
|
||||
|
||||
virtual void StartCopy() {}
|
||||
virtual bool CopyToStorage(const CopyJob& job) = 0;
|
||||
virtual void FinishCopy(bool success) {}
|
||||
|
@ -33,18 +33,24 @@ Organise::Organise(TaskManager* task_manager,
|
||||
const QStringList& files, bool eject_after)
|
||||
: thread_(NULL),
|
||||
task_manager_(task_manager),
|
||||
transcoder_(new Transcoder(this)),
|
||||
destination_(destination),
|
||||
format_(format),
|
||||
copy_(copy),
|
||||
overwrite_(overwrite),
|
||||
files_(files),
|
||||
eject_after_(eject_after),
|
||||
task_count_(files.count()),
|
||||
transcode_suffix_(1),
|
||||
started_(false),
|
||||
task_id_(0),
|
||||
progress_(0),
|
||||
song_progress_(0)
|
||||
{
|
||||
original_thread_ = thread();
|
||||
|
||||
foreach (const QString& filename, files) {
|
||||
tasks_pending_ << Task(filename);
|
||||
}
|
||||
}
|
||||
|
||||
void Organise::Start() {
|
||||
@ -56,6 +62,7 @@ void Organise::Start() {
|
||||
|
||||
thread_ = new QThread;
|
||||
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
|
||||
connect(transcoder_, SIGNAL(JobComplete(QString,bool)), SLOT(FileTranscoded(QString,bool)));
|
||||
|
||||
moveToThread(thread_);
|
||||
thread_->start();
|
||||
@ -63,12 +70,21 @@ void Organise::Start() {
|
||||
|
||||
void Organise::ProcessSomeFiles() {
|
||||
if (!started_) {
|
||||
transcode_temp_name_.open();
|
||||
supported_filetypes_ = destination_->SupportedFiletypes();
|
||||
|
||||
destination_->StartCopy();
|
||||
started_ = true;
|
||||
}
|
||||
|
||||
// None left?
|
||||
if (progress_ >= files_.count()) {
|
||||
if (tasks_pending_.isEmpty()) {
|
||||
if (!tasks_transcoding_.isEmpty()) {
|
||||
// Just wait - FileTranscoded will start us off again in a little while
|
||||
qDebug() << "Waiting for transcoding jobs";
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateProgress();
|
||||
|
||||
destination_->FinishCopy(files_with_errors_.isEmpty());
|
||||
@ -89,34 +105,75 @@ void Organise::ProcessSomeFiles() {
|
||||
return;
|
||||
}
|
||||
|
||||
QDir dir;
|
||||
|
||||
// We process files in batches so we can be cancelled part-way through.
|
||||
|
||||
const int n = qMin(files_.count(), progress_ + kBatchSize);
|
||||
for ( ; progress_<n ; ++progress_) {
|
||||
for (int i=0 ; i<kBatchSize ; ++i) {
|
||||
SetSongProgress(0);
|
||||
|
||||
const QString filename = files_[progress_];
|
||||
if (tasks_pending_.isEmpty())
|
||||
break;
|
||||
|
||||
Task task = tasks_pending_.takeFirst();
|
||||
qDebug() << "Processing" << task.filename_;
|
||||
|
||||
// Is it a directory?
|
||||
if (QFileInfo(filename).isDir()) {
|
||||
QDir dir(filename);
|
||||
if (QFileInfo(task.filename_).isDir()) {
|
||||
QDir dir(task.filename_);
|
||||
foreach (const QString& entry, dir.entryList(
|
||||
QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot | QDir::Readable)) {
|
||||
files_ << filename + "/" + entry;
|
||||
tasks_pending_ << Task(task.filename_ + "/" + entry);
|
||||
task_count_ ++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read metadata from the file
|
||||
Song song;
|
||||
song.InitFromFile(filename, -1);
|
||||
song.InitFromFile(task.filename_, -1);
|
||||
if (!song.is_valid())
|
||||
continue;
|
||||
|
||||
// Maybe this file is one that's been transcoded already?
|
||||
if (!task.transcoded_filename_.isEmpty()) {
|
||||
qDebug() << "This file has already been transcoded";
|
||||
|
||||
// Set the new filetype on the song so the formatter gets it right
|
||||
song.set_filetype(task.new_filetype_);
|
||||
|
||||
// Fiddle the filename extension as well to match the new type
|
||||
song.set_filename(song.filename().section('.', 0, -2) + "." + task.new_extension_);
|
||||
song.set_basefilename(song.basefilename().section('.', 0, -2) + "." + task.new_extension_);
|
||||
|
||||
// Have to set this to the size of the new file or else funny stuff happens
|
||||
song.set_filesize(QFileInfo(task.transcoded_filename_).size());
|
||||
} else {
|
||||
// Figure out if we need to transcode it
|
||||
Song::FileType dest_type = CheckTranscode(song.filetype());
|
||||
if (dest_type != Song::Type_Unknown) {
|
||||
// Get the preset
|
||||
TranscoderPreset preset = Transcoder::PresetForFileType(dest_type);
|
||||
qDebug() << "Transcoding with" << preset.name_;
|
||||
|
||||
// Get a temporary name for the transcoded file
|
||||
task.transcoded_filename_ = transcode_temp_name_.fileName() + "-" +
|
||||
QString::number(transcode_suffix_++);
|
||||
task.new_extension_ = preset.extension_;
|
||||
task.new_filetype_ = dest_type;
|
||||
tasks_transcoding_[task.filename_] = task;
|
||||
|
||||
qDebug() << "Transcoding to" << task.transcoded_filename_;
|
||||
|
||||
// Start the transcoding - this will happen in the background and
|
||||
// FileTranscoded() will get called when it's done. At that point the
|
||||
// task will get re-added to the pending queue with the new filename.
|
||||
transcoder_->AddJob(task.filename_, preset, task.transcoded_filename_);
|
||||
transcoder_->Start();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
MusicStorage::CopyJob job;
|
||||
job.source_ = filename;
|
||||
job.source_ = task.transcoded_filename_.isEmpty() ?
|
||||
task.filename_ : task.transcoded_filename_;
|
||||
job.destination_ = format_.GetFilenameForSong(song);
|
||||
job.metadata_ = song;
|
||||
job.overwrite_ = overwrite_;
|
||||
@ -124,14 +181,47 @@ void Organise::ProcessSomeFiles() {
|
||||
job.progress_ = boost::bind(&Organise::SetSongProgress, this, _1);
|
||||
|
||||
if (!destination_->CopyToStorage(job)) {
|
||||
files_with_errors_ << filename;
|
||||
files_with_errors_ << task.filename_;
|
||||
}
|
||||
|
||||
progress_++;
|
||||
}
|
||||
SetSongProgress(0);
|
||||
|
||||
QTimer::singleShot(0, this, SLOT(ProcessSomeFiles()));
|
||||
}
|
||||
|
||||
Song::FileType Organise::CheckTranscode(Song::FileType original_type) const {
|
||||
if (original_type == Song::Type_Stream)
|
||||
return Song::Type_Unknown;
|
||||
|
||||
const MusicStorage::TranscodeMode mode = destination_->GetTranscodeMode();
|
||||
const Song::FileType format = destination_->GetTranscodeFormat();
|
||||
|
||||
switch (mode) {
|
||||
case MusicStorage::Transcode_Never:
|
||||
return Song::Type_Unknown;
|
||||
|
||||
case MusicStorage::Transcode_Always:
|
||||
if (original_type == format)
|
||||
return Song::Type_Unknown;
|
||||
return format;
|
||||
|
||||
case MusicStorage::Transcode_Unsupported:
|
||||
if (supported_filetypes_.isEmpty() || supported_filetypes_.contains(original_type))
|
||||
return Song::Type_Unknown;
|
||||
|
||||
if (format != Song::Type_Unknown)
|
||||
return format;
|
||||
|
||||
// The user hasn't visited the device properties page yet to set a
|
||||
// preferred format for the device, so we have to pick the best
|
||||
// available one.
|
||||
return Transcoder::PickBestFormat(supported_filetypes_);
|
||||
}
|
||||
return Song::Type_Unknown;
|
||||
}
|
||||
|
||||
void Organise::SetSongProgress(float progress) {
|
||||
song_progress_ = qBound(0, int(progress * 100), 99);
|
||||
UpdateProgress();
|
||||
@ -139,6 +229,18 @@ void Organise::SetSongProgress(float progress) {
|
||||
|
||||
void Organise::UpdateProgress() {
|
||||
const int progress = progress_ * 100 + song_progress_;
|
||||
const int total = files_.count() * 100;
|
||||
const int total = task_count_ * 100;
|
||||
task_manager_->SetTaskProgress(task_id_, progress, total);
|
||||
}
|
||||
|
||||
void Organise::FileTranscoded(const QString& filename, bool success) {
|
||||
qDebug() << "File finished" << filename << success;
|
||||
|
||||
Task task = tasks_transcoding_.take(filename);
|
||||
if (!success) {
|
||||
files_with_errors_ << filename;
|
||||
} else {
|
||||
tasks_pending_ << task;
|
||||
}
|
||||
QTimer::singleShot(0, this, SLOT(ProcessSomeFiles()));
|
||||
}
|
||||
|
@ -18,10 +18,12 @@
|
||||
#define ORGANISE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
#include "organiseformat.h"
|
||||
#include "transcoder/transcoder.h"
|
||||
|
||||
class MusicStorage;
|
||||
class TaskManager;
|
||||
@ -44,20 +46,41 @@ signals:
|
||||
|
||||
private slots:
|
||||
void ProcessSomeFiles();
|
||||
void SetSongProgress(float progress);
|
||||
void UpdateProgress();
|
||||
void FileTranscoded(const QString& filename, bool success);
|
||||
|
||||
private:
|
||||
void SetSongProgress(float progress);
|
||||
void UpdateProgress();
|
||||
Song::FileType CheckTranscode(Song::FileType original_type) const;
|
||||
|
||||
private:
|
||||
struct Task {
|
||||
Task(const QString& filename = QString()) : filename_(filename) {}
|
||||
|
||||
QString filename_;
|
||||
QString transcoded_filename_;
|
||||
QString new_extension_;
|
||||
Song::FileType new_filetype_;
|
||||
};
|
||||
|
||||
QThread* thread_;
|
||||
QThread* original_thread_;
|
||||
TaskManager* task_manager_;
|
||||
Transcoder* transcoder_;
|
||||
boost::shared_ptr<MusicStorage> destination_;
|
||||
QList<Song::FileType> supported_filetypes_;
|
||||
|
||||
const OrganiseFormat format_;
|
||||
const bool copy_;
|
||||
const bool overwrite_;
|
||||
QStringList files_;
|
||||
const bool eject_after_;
|
||||
int task_count_;
|
||||
|
||||
QTemporaryFile transcode_temp_name_;
|
||||
int transcode_suffix_;
|
||||
|
||||
QList<Task> tasks_pending_;
|
||||
QMap<QString, Task> tasks_transcoding_;
|
||||
|
||||
bool started_;
|
||||
|
||||
|
@ -94,3 +94,15 @@ void ConnectedDevice::FinishCopy(bool) {
|
||||
void ConnectedDevice::FinishDelete(bool) {
|
||||
lister_->UpdateDeviceFreeSpace(unique_id_);
|
||||
}
|
||||
|
||||
MusicStorage::TranscodeMode ConnectedDevice::GetTranscodeMode() const {
|
||||
int index = manager_->FindDeviceById(unique_id_);
|
||||
return MusicStorage::TranscodeMode(
|
||||
manager_->index(index).data(DeviceManager::Role_TranscodeMode).toInt());
|
||||
}
|
||||
|
||||
Song::FileType ConnectedDevice::GetTranscodeFormat() const {
|
||||
int index = manager_->FindDeviceById(unique_id_);
|
||||
return Song::FileType(
|
||||
manager_->index(index).data(DeviceManager::Role_TranscodeFormat).toInt());
|
||||
}
|
||||
|
@ -44,7 +44,8 @@ public:
|
||||
|
||||
virtual void Init() = 0;
|
||||
|
||||
virtual QList<Song::FileType> SupportedFiletypes() { return QList<Song::FileType>(); }
|
||||
virtual TranscodeMode GetTranscodeMode() const;
|
||||
virtual Song::FileType GetTranscodeFormat() const;
|
||||
|
||||
DeviceLister* lister() const { return lister_; }
|
||||
QString unique_id() const { return unique_id_; }
|
||||
|
@ -52,7 +52,7 @@ DeviceDatabaseBackend::DeviceList DeviceDatabaseBackend::GetAllDevices() {
|
||||
dev.friendly_name_ = q.value(2).toString();
|
||||
dev.size_ = q.value(3).toLongLong();
|
||||
dev.icon_name_ = q.value(4).toString();
|
||||
dev.transcode_mode_ = TranscodeMode(q.value(5).toInt());
|
||||
dev.transcode_mode_ = MusicStorage::TranscodeMode(q.value(5).toInt());
|
||||
dev.transcode_format_ = Song::FileType(q.value(6).toInt());
|
||||
ret << dev;
|
||||
}
|
||||
@ -118,7 +118,7 @@ void DeviceDatabaseBackend::RemoveDevice(int id) {
|
||||
|
||||
void DeviceDatabaseBackend::SetDeviceOptions(int id,
|
||||
const QString &friendly_name, const QString &icon_name,
|
||||
TranscodeMode mode, Song::FileType format) {
|
||||
MusicStorage::TranscodeMode mode, Song::FileType format) {
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class Database;
|
||||
@ -31,13 +32,6 @@ class DeviceDatabaseBackend : public QObject {
|
||||
public:
|
||||
Q_INVOKABLE DeviceDatabaseBackend(QObject* parent = 0);
|
||||
|
||||
// Values are saved in the database - don't change
|
||||
enum TranscodeMode {
|
||||
Transcode_Always = 1,
|
||||
Transcode_Never = 2,
|
||||
Transcode_Unsupported = 3,
|
||||
};
|
||||
|
||||
struct Device {
|
||||
Device() : id_(-1) {}
|
||||
|
||||
@ -47,7 +41,7 @@ public:
|
||||
quint64 size_;
|
||||
QString icon_name_;
|
||||
|
||||
TranscodeMode transcode_mode_;
|
||||
MusicStorage::TranscodeMode transcode_mode_;
|
||||
Song::FileType transcode_format_;
|
||||
};
|
||||
typedef QList<Device> DeviceList;
|
||||
@ -63,7 +57,7 @@ public:
|
||||
|
||||
void SetDeviceOptions(int id,
|
||||
const QString& friendly_name, const QString& icon_name,
|
||||
TranscodeMode mode, Song::FileType format);
|
||||
MusicStorage::TranscodeMode mode, Song::FileType format);
|
||||
|
||||
private:
|
||||
boost::shared_ptr<Database> db_;
|
||||
|
@ -60,7 +60,7 @@ const int DeviceManager::kDeviceIconOverlaySize = 16;
|
||||
|
||||
DeviceManager::DeviceInfo::DeviceInfo()
|
||||
: database_id_(-1),
|
||||
transcode_mode_(DeviceDatabaseBackend::Transcode_Unsupported),
|
||||
transcode_mode_(MusicStorage::Transcode_Unsupported),
|
||||
transcode_format_(Song::Type_Unknown),
|
||||
task_percentage_(-1)
|
||||
{
|
||||
@ -618,7 +618,7 @@ void DeviceManager::Forget(int row) {
|
||||
|
||||
void DeviceManager::SetDeviceOptions(int row,
|
||||
const QString& friendly_name, const QString& icon_name,
|
||||
DeviceDatabaseBackend::TranscodeMode mode, Song::FileType format) {
|
||||
MusicStorage::TranscodeMode mode, Song::FileType format) {
|
||||
DeviceInfo& info = devices_[row];
|
||||
info.friendly_name_ = friendly_name;
|
||||
info.LoadIcon(QVariantList() << icon_name, friendly_name);
|
||||
@ -676,6 +676,8 @@ void DeviceManager::TasksChanged() {
|
||||
DeviceInfo& info = devices_[index.row()];
|
||||
info.task_percentage_ = -1;
|
||||
emit dataChanged(index, index);
|
||||
|
||||
active_tasks_.remove(active_tasks_.key(index));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ public:
|
||||
|
||||
void SetDeviceOptions(int row,
|
||||
const QString& friendly_name, const QString& icon_name,
|
||||
DeviceDatabaseBackend::TranscodeMode mode, Song::FileType format);
|
||||
MusicStorage::TranscodeMode mode, Song::FileType format);
|
||||
|
||||
// QAbstractListModel
|
||||
int rowCount(const QModelIndex &parent) const;
|
||||
@ -151,7 +151,7 @@ private:
|
||||
QString icon_name_;
|
||||
QIcon icon_;
|
||||
|
||||
DeviceDatabaseBackend::TranscodeMode transcode_mode_;
|
||||
MusicStorage::TranscodeMode transcode_mode_;
|
||||
Song::FileType transcode_format_;
|
||||
|
||||
int task_percentage_;
|
||||
|
@ -187,18 +187,18 @@ void DeviceProperties::UpdateFormats() {
|
||||
manager_->GetConnectedDevice(index_.row());
|
||||
|
||||
// Transcode mode
|
||||
DeviceDatabaseBackend::TranscodeMode mode = DeviceDatabaseBackend::TranscodeMode(
|
||||
MusicStorage::TranscodeMode mode = MusicStorage::TranscodeMode(
|
||||
index_.data(DeviceManager::Role_TranscodeMode).toInt());
|
||||
switch (mode) {
|
||||
case DeviceDatabaseBackend::Transcode_Always:
|
||||
case MusicStorage::Transcode_Always:
|
||||
ui_->transcode_all->setChecked(true);
|
||||
break;
|
||||
|
||||
case DeviceDatabaseBackend::Transcode_Never:
|
||||
case MusicStorage::Transcode_Never:
|
||||
ui_->transcode_off->setChecked(true);
|
||||
break;
|
||||
|
||||
case DeviceDatabaseBackend::Transcode_Unsupported:
|
||||
case MusicStorage::Transcode_Unsupported:
|
||||
default:
|
||||
ui_->transcode_unsupported->setChecked(true);
|
||||
break;
|
||||
@ -238,13 +238,13 @@ void DeviceProperties::accept() {
|
||||
QDialog::accept();
|
||||
|
||||
// Transcode mode
|
||||
DeviceDatabaseBackend::TranscodeMode mode = DeviceDatabaseBackend::Transcode_Unsupported;
|
||||
MusicStorage::TranscodeMode mode = MusicStorage::Transcode_Unsupported;
|
||||
if (ui_->transcode_all->isChecked())
|
||||
mode = DeviceDatabaseBackend::Transcode_Always;
|
||||
mode = MusicStorage::Transcode_Always;
|
||||
else if (ui_->transcode_off->isChecked())
|
||||
mode = DeviceDatabaseBackend::Transcode_Never;
|
||||
mode = MusicStorage::Transcode_Never;
|
||||
else if (ui_->transcode_unsupported->isChecked())
|
||||
mode = DeviceDatabaseBackend::Transcode_Unsupported;
|
||||
mode = MusicStorage::Transcode_Unsupported;
|
||||
|
||||
// Transcode format
|
||||
Song::FileType format = Song::FileType(ui_->transcode_format->itemData(
|
||||
@ -288,23 +288,7 @@ void DeviceProperties::UpdateFormatsFinished() {
|
||||
if (preset.type_ == Song::Type_Unknown) {
|
||||
// The user hasn't chosen a format for this device yet, so work our way down
|
||||
// a list of some preferred formats, picking the first one that is supported
|
||||
QList<Song::FileType> best_formats;
|
||||
best_formats << Song::Type_Mpeg;
|
||||
best_formats << Song::Type_OggVorbis;
|
||||
best_formats << Song::Type_Asf;
|
||||
|
||||
foreach (Song::FileType type, best_formats) {
|
||||
if (list.isEmpty() || list.contains(type)) {
|
||||
preset = Transcoder::PresetForFileType(type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (preset.type_ == Song::Type_Unknown) {
|
||||
// Still haven't found a good format - pick the first one that the
|
||||
// device advertises.
|
||||
preset = Transcoder::PresetForFileType(list[0]);
|
||||
}
|
||||
preset = Transcoder::PresetForFileType(Transcoder::PickBestFormat(list));
|
||||
}
|
||||
ui_->transcode_format->setCurrentIndex(ui_->transcode_format->findText(preset.name_));
|
||||
|
||||
|
@ -84,6 +84,9 @@ static int ProgressCallback(uint64_t const sent, uint64_t const total,
|
||||
}
|
||||
|
||||
bool MtpDevice::CopyToStorage(const CopyJob& job) {
|
||||
if (!connection_->is_valid())
|
||||
return false;
|
||||
|
||||
// Convert metadata
|
||||
LIBMTP_track_t track;
|
||||
job.metadata_.ToMTP(&track);
|
||||
@ -165,6 +168,11 @@ QList<Song::FileType> MtpDevice::SupportedFiletypes() {
|
||||
|
||||
QMutexLocker l(&db_busy_);
|
||||
MtpConnection connection(url_.host());
|
||||
if (!connection.is_valid()) {
|
||||
qWarning() << "Error connecting to MTP device, couldn't get list of supported filetypes";
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (LIBMTP_Get_Supported_Filetypes(connection.device(), &list, &length)
|
||||
|| !list || !length)
|
||||
return ret;
|
||||
|
@ -206,6 +206,23 @@ TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
|
||||
}
|
||||
}
|
||||
|
||||
Song::FileType Transcoder::PickBestFormat(QList<Song::FileType> supported) {
|
||||
if (supported.isEmpty())
|
||||
return Song::Type_Unknown;
|
||||
|
||||
QList<Song::FileType> best_formats;
|
||||
best_formats << Song::Type_Mpeg;
|
||||
best_formats << Song::Type_OggVorbis;
|
||||
best_formats << Song::Type_Asf;
|
||||
|
||||
foreach (Song::FileType type, best_formats) {
|
||||
if (supported.isEmpty() || supported.contains(type))
|
||||
return type;
|
||||
}
|
||||
|
||||
return supported[0];
|
||||
}
|
||||
|
||||
void Transcoder::AddJob(const QString& input,
|
||||
const TranscoderPreset& preset,
|
||||
const QString& output) {
|
||||
|
@ -55,6 +55,7 @@ class Transcoder : public QObject {
|
||||
|
||||
static TranscoderPreset PresetForFileType(Song::FileType type);
|
||||
static QList<TranscoderPreset> GetAllPresets();
|
||||
static Song::FileType PickBestFormat(QList<Song::FileType> supported);
|
||||
|
||||
int max_threads() const { return max_threads_; }
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user