Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/app/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
#include "base/search/searchpluginmanager.h"
#include "base/settingsstorage.h"
#include "base/torrentfileswatcher.h"
#include "base/torrentgroup.h"
#include "base/utils/fs.h"
#include "base/utils/misc.h"
#include "base/utils/os.h"
Expand Down Expand Up @@ -306,6 +307,9 @@ Application::Application(int &argc, char **argv)

initializeTranslation();

// Load persisted torrent groups
TorrentGroupManager::instance()->load();

connect(this, &QCoreApplication::aboutToQuit, this, &Application::cleanup);
connect(m_instanceManager, &ApplicationInstanceManager::messageReceived, this, &Application::processMessage);
#if defined(Q_OS_WIN) && !defined(DISABLE_GUI)
Expand Down Expand Up @@ -1349,6 +1353,10 @@ void Application::cleanup()

LogMsg(tr("qBittorrent termination initiated"));

// Persist torrent groups before tearing down preferences
if (TorrentGroupManager::instance())
TorrentGroupManager::instance()->save();

#ifndef DISABLE_GUI
if (m_desktopIntegration)
{
Expand Down
2 changes: 2 additions & 0 deletions src/base/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ add_library(qbt_base STATIC
settingsstorage.h
tag.h
tagset.h
torrentgroup.h
torrentfileguard.h
torrentfileswatcher.h
torrentfilter.h
Expand Down Expand Up @@ -198,6 +199,7 @@ add_library(qbt_base STATIC
settingsstorage.cpp
tag.cpp
tagset.cpp
torrentgroup.cpp
torrentfileguard.cpp
torrentfileswatcher.cpp
torrentfilter.cpp
Expand Down
216 changes: 216 additions & 0 deletions src/base/torrentgroup.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 AlfEspadero
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/

#include "torrentgroup.h"

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>

#include "base/settingvalue.h"
#include "base/bittorrent/infohash.h"

namespace
{
const QString kPrefKey = QStringLiteral("TorrentGroups/Groups");
}

TorrentGroupManager *TorrentGroupManager::m_instance = nullptr;

TorrentGroupManager::TorrentGroupManager(QObject *parent)
: QObject(parent)
{
}

TorrentGroupManager *TorrentGroupManager::instance()
{
static TorrentGroupManager guard; // static lifetime
if (!m_instance)
m_instance = &guard;
return m_instance;
}

QList<TorrentGroup> TorrentGroupManager::groups() const
{
return m_groups.values();
}

bool TorrentGroupManager::hasGroup(const QString &name) const
{
return m_groups.contains(name);
}

TorrentGroup TorrentGroupManager::group(const QString &name) const
{
return m_groups.value(name, {});
}

bool TorrentGroupManager::createGroup(const QString &name, const QSet<BitTorrent::TorrentID> &initialMembers)
{
const QString trimmed = name.trimmed();
if (trimmed.isEmpty() || hasGroup(trimmed))
return false;
TorrentGroup g;
g.name = trimmed;
g.members = initialMembers;
m_groups.insert(trimmed, g);
emit groupsChanged();
if (!initialMembers.isEmpty())
emit groupMembershipChanged(trimmed);
return true;
}

bool TorrentGroupManager::renameGroup(const QString &oldName, const QString &newName)
{
if (!hasGroup(oldName))
return false;
const QString trimmed = newName.trimmed();
if (trimmed.isEmpty() || hasGroup(trimmed))
return false;
TorrentGroup g = m_groups.take(oldName);
g.name = trimmed;
m_groups.insert(trimmed, g);
// migrate expanded state if needed
if (m_expandedGroups.contains(oldName))
{
m_expandedGroups.removeAll(oldName);
if (!m_expandedGroups.contains(trimmed))
m_expandedGroups << trimmed;
save(); // persist change including expansion mapping
}
emit groupsChanged();
return true;
}

bool TorrentGroupManager::deleteGroup(const QString &name)
{
if (!hasGroup(name))
return false;
m_groups.remove(name);
emit groupsChanged();
return true;
}

bool TorrentGroupManager::addMembers(const QString &groupName, const QSet<BitTorrent::TorrentID> &members)
{
if (!hasGroup(groupName) || members.isEmpty())
return false;
TorrentGroup &g = m_groups[groupName];
const int oldSize = g.members.size();
g.members.unite(members);
if (g.members.size() != oldSize)
emit groupMembershipChanged(groupName);
return true;
}

bool TorrentGroupManager::removeMembers(const QString &groupName, const QSet<BitTorrent::TorrentID> &members)
{
if (!hasGroup(groupName) || members.isEmpty())
return false;
TorrentGroup &g = m_groups[groupName];
bool changed = false;
for (const BitTorrent::TorrentID &id : members)
changed |= g.members.remove(id) > 0;
if (changed)
emit groupMembershipChanged(groupName);
return changed;
}

QString TorrentGroupManager::groupOf(const BitTorrent::TorrentID &id) const
{
for (const TorrentGroup &g : m_groups)
{
if (g.members.contains(id))
return g.name;
}
return {};
}

void TorrentGroupManager::load()
{
m_groups.clear();
m_expandedGroups.clear();
SettingValue<QByteArray> rawSetting {kPrefKey};
const QByteArray raw = rawSetting.get();
if (raw.isEmpty())
return;
const QJsonDocument doc = QJsonDocument::fromJson(raw);
if (!doc.isObject())
return;
const QJsonObject root = doc.object();
const QJsonArray groupsArr = root.value(QStringLiteral("groups")).toArray();
for (const QJsonValue &val : groupsArr)
{
if (!val.isObject())
continue;
const QJsonObject obj = val.toObject();
const QString name = obj.value(QStringLiteral("name")).toString();
if (name.trimmed().isEmpty())
continue;
TorrentGroup g;
g.name = name;
const QJsonArray memArr = obj.value(QStringLiteral("members")).toArray();
for (const QJsonValue &mVal : memArr)
g.members.insert(BitTorrent::TorrentID::fromString(mVal.toString()));
m_groups.insert(g.name, g);
}
const QJsonArray expandedArr = root.value(QStringLiteral("expanded")).toArray();
for (const QJsonValue &v : expandedArr)
m_expandedGroups << v.toString();
emit groupsChanged();
}

void TorrentGroupManager::save() const
{
QJsonArray groupsArr;
for (const TorrentGroup &g : m_groups)
{
QJsonObject obj;
obj.insert(QStringLiteral("name"), g.name);
QJsonArray memArr;
for (const BitTorrent::TorrentID &id : g.members)
memArr.append(id.toString());
obj.insert(QStringLiteral("members"), memArr);
groupsArr.append(obj);
}
QJsonArray expandedArr;
for (const QString &n : m_expandedGroups)
expandedArr.append(n);
QJsonObject root;
root.insert(QStringLiteral("groups"), groupsArr);
root.insert(QStringLiteral("expanded"), expandedArr);
const QJsonDocument doc(root);
SettingValue<QByteArray> rawSetting {kPrefKey};
rawSetting = doc.toJson(QJsonDocument::Compact);
}

void TorrentGroupManager::setExpandedGroups(const QStringList &names)
{
m_expandedGroups = names;
save(); // persist immediately for now
}
86 changes: 86 additions & 0 deletions src/base/torrentgroup.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 AlfEspadero
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/

#pragma once

#include <QHash>
#include <QSet>
#include <QString>
#include <QObject>

#include "base/bittorrent/infohash.h" // for BitTorrent::TorrentID

struct TorrentGroup
{
QString name; // unique
QSet<BitTorrent::TorrentID> members;
bool isValid() const
{
return !name.trimmed().isEmpty();
}
};

class TorrentGroupManager final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(TorrentGroupManager)

explicit TorrentGroupManager(QObject *parent = nullptr);
public:
static TorrentGroupManager *instance();

QList<TorrentGroup> groups() const;
bool hasGroup(const QString &name) const;
TorrentGroup group(const QString &name) const;

bool createGroup(const QString &name, const QSet<BitTorrent::TorrentID> &initialMembers = {});
bool renameGroup(const QString &oldName, const QString &newName);
bool deleteGroup(const QString &name);
bool addMembers(const QString &groupName, const QSet<BitTorrent::TorrentID> &members);
bool removeMembers(const QString &groupName, const QSet<BitTorrent::TorrentID> &members);

QString groupOf(const BitTorrent::TorrentID &id) const; // single group per torrent for MVP

void load();
void save() const;

QStringList expandedGroups() const
{
return m_expandedGroups;
}
void setExpandedGroups(const QStringList &names);

signals:
void groupsChanged();
void groupMembershipChanged(const QString &groupName);

private:
static TorrentGroupManager *m_instance;
QHash<QString, TorrentGroup> m_groups; // key: name
QStringList m_expandedGroups;
};
2 changes: 2 additions & 0 deletions src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ add_library(qbt_gui STATIC
transferlistfilters/trackersfilterwidget.h
transferlistfilterswidget.h
transferlistmodel.h
transferlistgroupmodel.h
transferlistsortmodel.h
transferlistwidget.h
tristateaction.h
Expand Down Expand Up @@ -232,6 +233,7 @@ add_library(qbt_gui STATIC
transferlistfilters/trackersfilterwidget.cpp
transferlistfilterswidget.cpp
transferlistmodel.cpp
transferlistgroupmodel.cpp
transferlistsortmodel.cpp
transferlistwidget.cpp
tristateaction.cpp
Expand Down
Loading
Loading