10 Commits

45 changed files with 1986 additions and 420 deletions

View File

@@ -5,6 +5,10 @@ find_package(Qt6 REQUIRED COMPONENTS Core Network Concurrent Widgets)
AddFGLExecutable(HydrusImporter ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(HydrusImporter PUBLIC Qt6::Core Qt6::Concurrent IDHANClient sqlite3 spdlog Qt6::Widgets)
file(GLOB_RECURSE UI_FILES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/**.ui)
target_sources(HydrusImporter PRIVATE ${UI_FILES})
if (DEFINED IMPORTER_TESTS AND IMPORTER_TESTS EQUAL 1)
target_compile_definitions(HydrusImporter PUBLIC IMPORTER_TESTS=1)
endif ()

View File

@@ -50,15 +50,99 @@ HydrusImporter::~HydrusImporter()
sqlite3_close_v2( mappings_db );
}
std::unordered_map< HashID, RecordID > HydrusImporter::mapHydrusRecords( std::vector< HashID > hash_ids ) const
{
idhan::hydrus::TransactionBase master_tr { master_db };
if ( hash_ids.empty() ) return {};
std::ranges::sort( hash_ids );
std::ranges::unique( hash_ids );
std::unordered_map< std::uint32_t, std::string > hashes_map {};
hashes_map.reserve( hash_ids.size() );
for ( const auto& hash_id : hash_ids )
{
master_tr << "SELECT hex(hash) FROM hashes WHERE hash_id = $1" << hash_id >>
[ & ]( const std::string_view hash_i )
{
if ( hash_i.size() != ( 256 / 8 * 2 ) )
{
return;
}
hashes_map.emplace( hash_id, hash_i );
};
}
std::vector< std::string > hashes {};
hashes.reserve( hash_ids.size() );
for ( const auto& hash : hashes_map | std::views::values ) hashes.emplace_back( hash );
auto& client { idhan::IDHANClient::instance() };
auto created_records { client.createRecords( hashes ) };
created_records.waitForFinished();
const auto created_records_result { created_records.result<>() };
if ( created_records_result.size() != hashes.size() ) throw std::runtime_error( "Failed to create records" );
std::unordered_map< std::string, RecordID > record_map {};
for ( const auto& [ record_id, hash ] : std::ranges::views::zip( created_records_result, hashes ) )
{
record_map.emplace( hash, record_id );
}
std::unordered_map< HashID, RecordID > record_id_map {};
record_id_map.reserve( hash_ids.size() );
for ( const auto& [ hy_id, hash ] : hashes_map )
{
const auto record_id { record_map.at( hash ) };
record_id_map.emplace( hy_id, record_id );
}
return record_id_map;
}
RecordID HydrusImporter::getRecordIDFromHyID( const HashID hash_id )
{
idhan::hydrus::TransactionBaseCoro client_tr { client_db };
idhan::hydrus::Query< std::string_view > query {
client_tr, "SELECT hex(hash) FROM hashes WHERE hash_id = $1", hash_id
};
auto& client { IDHANClient::instance() };
std::vector< std::string > hashes {};
for ( const auto& [ hash ] : query )
{
hashes.emplace_back( hash );
}
auto future { client.getRecordID( hashes.front() ) };
future.waitForFinished();
const auto result { future.result<>() };
if ( !result ) throw std::runtime_error( "Failed to get record from client" );
return result.value();
}
bool HydrusImporter::hasPTR() const
{
bool exists { false };
TransactionBaseCoro client_tr { client_db };
Query< std::size_t, std::string_view > query { client_tr,
"SELECT service_id, name FROM services WHERE service_type = $1",
static_cast< int >( hy_constants::ServiceTypes::PTR_SERVICE ) };
Query< std::size_t, std::string_view > query {
client_tr,
"SELECT service_id, name FROM services WHERE service_type = $1",
static_cast< int >( hy_constants::ServiceTypes::PTR_SERVICE )
};
for ( [[maybe_unused]] const auto& [ serviae_id, name ] : query )
{

View File

@@ -27,6 +27,8 @@ struct ServiceInfo
ServiceInfo() = default;
};
using HashID = std::uint32_t;
class HydrusImporter
{
public:
@@ -51,6 +53,9 @@ class HydrusImporter
HydrusImporter( const std::filesystem::path& path );
~HydrusImporter();
std::unordered_map< HashID, RecordID > mapHydrusRecords( std::vector< HashID > hash_ids ) const;
RecordID getRecordIDFromHyID( HashID hash_id );
bool hasPTR() const;
std::vector< ServiceInfo > getTagServices();

View File

@@ -5,15 +5,16 @@
#include "HydrusImporterWidget.hpp"
#include <moc_HydrusImporterWidget.cpp>
#include <QFileDialog>
#include <QFileInfo>
#include <ui_TagServiceWidget.h>
#include "HydrusImporter.hpp"
#include "TagServiceWidget.hpp"
#include "TagServiceWorker.hpp"
#include "file_relationships/FileRelationshipsWidget.hpp"
#include "tag_service/TagServiceWidget.hpp"
#include "ui_HydrusImporterWidget.h"
#include "urls/UrlServiceWidget.hpp"
class TagServiceWorker;
@@ -41,6 +42,93 @@ HydrusImporterWidget::~HydrusImporterWidget()
delete ui;
}
void HydrusImporterWidget::parseTagServices()
{
auto service_infos { m_importer->getTagServices() };
for ( const auto& service : service_infos )
{
if ( service.name == "public tag repository" && !ui->cbProcessPTR->isChecked() )
{
// idhan::logging::info( "Skipping PTR because cbProcessPTR is not checked" );
continue;
}
auto widget { new TagServiceWidget( m_importer.get(), this ) };
widget->setName( service.name );
widget->setInfo( service );
// ui->tagServicesLayout->addWidget( widget );
addServiceWidget( widget );
connect(
this,
&HydrusImporterWidget::triggerImport,
widget,
&TagServiceWidget::startImport,
Qt::SingleShotConnection );
connect(
this,
&HydrusImporterWidget::triggerPreImport,
widget,
&TagServiceWidget::startPreImport,
Qt::SingleShotConnection );
}
}
void HydrusImporterWidget::addServiceWidget( QWidget* widget )
{
auto* groupFrame = new QFrame( this );
groupFrame->setFrameShape( QFrame::Box );
groupFrame->setFrameShadow( QFrame::Plain );
groupFrame->setLineWidth( 1 );
groupFrame->setStyleSheet( "QFrame { border: 1px solid #444; border-radius: 6px; }" );
auto* groupLayout = new QVBoxLayout( groupFrame );
groupLayout->setContentsMargins( 6, 6, 6, 6 );
groupLayout->addWidget( widget );
widget->setStyleSheet( "QFrame { border: none; }" );
ui->tagServicesLayout->addWidget( groupFrame );
}
void HydrusImporterWidget::parseFileRelationships()
{
auto* widget { new FileRelationshipsWidget( m_importer.get() ) };
connect(
this,
&HydrusImporterWidget::triggerImport,
widget,
&FileRelationshipsWidget::startImport,
Qt::SingleShotConnection );
connect(
this,
&HydrusImporterWidget::triggerPreImport,
widget,
&FileRelationshipsWidget::startPreImport,
Qt::SingleShotConnection );
addServiceWidget( widget );
}
void HydrusImporterWidget::parseUrls()
{
auto* widget { new UrlServiceWidget( m_importer.get() ) };
connect(
this, &HydrusImporterWidget::triggerImport, widget, &UrlServiceWidget::startImport, Qt::SingleShotConnection );
connect(
this,
&HydrusImporterWidget::triggerPreImport,
widget,
&UrlServiceWidget::startPreImport,
Qt::SingleShotConnection );
addServiceWidget( widget );
}
void HydrusImporterWidget::on_hydrusFolderPath_textChanged( [[maybe_unused]] const QString& path )
{
testHydrusPath();
@@ -101,39 +189,11 @@ void HydrusImporterWidget::on_parseHydrusDB_pressed()
m_importer = std::make_unique< idhan::hydrus::HydrusImporter >( ui->hydrusFolderPath->text().toStdString() );
const bool has_ptr { m_importer->hasPTR() };
auto service_infos { m_importer->getTagServices() };
ui->parseStatusLabel->setText( QString( "Has PTR: %1" ).arg( has_ptr ? "Yes" : "No" ) );
for ( const auto& service : service_infos )
{
if ( service.name == "public tag repository" && !ui->cbProcessPTR->isChecked() )
{
// idhan::logging::info( "Skipping PTR because cbProcessPTR is not checked" );
continue;
}
auto widget { new TagServiceWidget( m_importer.get(), this ) };
widget->setName( service.name );
widget->setInfo( service );
ui->tagServicesLayout->addWidget( widget );
connect(
this,
&HydrusImporterWidget::triggerImport,
widget,
&TagServiceWidget::startImport,
Qt::SingleShotConnection );
connect(
this,
&HydrusImporterWidget::triggerPreImport,
widget,
&TagServiceWidget::startPreImport,
Qt::SingleShotConnection );
}
parseTagServices();
parseFileRelationships();
parseUrls();
emit triggerPreImport();

View File

@@ -36,6 +36,11 @@ class HydrusImporterWidget final : public QWidget
explicit HydrusImporterWidget( QWidget* parent = nullptr );
~HydrusImporterWidget() override;
void parseTagServices();
void addServiceWidget( QWidget* widget );
void parseFileRelationships();
void parseUrls();
public slots:
void on_hydrusFolderPath_textChanged( const QString& path );
void testHydrusPath();

View File

@@ -1,208 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TagServiceWidget</class>
<widget class="QWidget" name="TagServiceWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>691</width>
<height>96</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QFrame" name="frame_3">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="name">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Name: </string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(0, 170, 0);</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>6</number>
</property>
<item alignment="Qt::AlignmentFlag::AlignLeft">
<widget class="QLabel" name="mappingsCount">
<property name="text">
<string>Mappings: 0</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignmentFlag::AlignLeft">
<widget class="QLabel" name="parentsCount">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Parents/Children: 0</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignmentFlag::AlignLeft">
<widget class="QLabel" name="aliasesCount">
<property name="text">
<string>Aliases: 0</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="cbShouldImport">
<property name="text">
<string>Import</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Only processes mappings for files that you've obtained&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Only acquired files</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,97 @@
//
// Created by kj16609 on 11/5/25.
//
#include "FileRelationshipsWidget.hpp"
#include "FileRelationshipsWorker.hpp"
#include "ui_FileRelationshipsWidget.h"
FileRelationshipsWidget::FileRelationshipsWidget( idhan::hydrus::HydrusImporter* importer, QWidget* parent ) :
QWidget( parent ),
m_importer( importer ),
m_worker( new FileRelationshipsWorker( this, importer ) ),
ui( new Ui::FileRelationshipsWidget() )
{
ui->setupUi( this );
connect(
m_worker,
&FileRelationshipsWorker::processedMaxDuplicates,
this,
&FileRelationshipsWidget::processedMaxDuplicates );
connect(
m_worker,
&FileRelationshipsWorker::processedMaxAlternatives,
this,
&FileRelationshipsWidget::processedMaxAlternatives );
connect(
m_worker, &FileRelationshipsWorker::processedDuplicates, this, &FileRelationshipsWidget::processedDuplicates );
connect(
m_worker,
&FileRelationshipsWorker::processedAlternatives,
this,
&FileRelationshipsWidget::processedAlternatives );
connect( m_worker, &FileRelationshipsWorker::statusMessage, this, &FileRelationshipsWidget::statusMessage );
}
void FileRelationshipsWidget::updateText()
{
if ( alternatives_processed > 0 || duplicates_processed > 0 )
{
ui->alternativesCount->setText(
QString( "Alternative groups: %L1 (%L2 processed)" )
.arg( alternatives_total )
.arg( alternatives_processed ) );
ui->duplicatesCount->setText(
QString( "Duplicate pairs: %L1 (%L2 processed)" ).arg( duplicates_total ).arg( duplicates_processed ) );
}
else
{
ui->alternativesCount->setText( QString( "Alternative groups: %L1" ).arg( alternatives_total ) );
ui->duplicatesCount->setText( QString( "Duplicate pairs: %L1" ).arg( duplicates_total ) );
}
ui->progressBar->setMaximum( alternatives_total + duplicates_total );
ui->progressBar->setValue( alternatives_processed + duplicates_processed );
}
void FileRelationshipsWidget::startImport()
{
if ( ui->cbShouldImport->isChecked() ) QThreadPool::globalInstance()->start( m_worker );
}
void FileRelationshipsWidget::startPreImport()
{
QThreadPool::globalInstance()->start( m_worker );
}
void FileRelationshipsWidget::statusMessage( const QString& msg )
{
ui->statusLabel->setText( msg );
}
void FileRelationshipsWidget::processedDuplicates( const std::size_t count )
{
duplicates_processed = count;
updateText();
}
void FileRelationshipsWidget::processedMaxDuplicates( const std::size_t count )
{
duplicates_total = count;
updateText();
}
void FileRelationshipsWidget::processedAlternatives( const std::size_t count )
{
alternatives_processed = count;
updateText();
}
void FileRelationshipsWidget::processedMaxAlternatives( const std::size_t count )
{
alternatives_total = count;
updateText();
}

View File

@@ -0,0 +1,47 @@
//
// Created by kj16609 on 11/5/25.
//
#pragma once
#include "FileRelationshipsWorker.hpp"
#include "gui/hydrus/HydrusImporterWidget.hpp"
namespace Ui
{
class FileRelationshipsWidget;
}
class FileRelationshipsWidget : public QWidget
{
idhan::hydrus::HydrusImporter* m_importer;
std::size_t alternatives_processed { 0 };
std::size_t alternatives_total { 0 };
std::size_t duplicates_processed { 0 };
std::size_t duplicates_total { 0 };
FileRelationshipsWorker* m_worker;
public:
Q_DISABLE_COPY_MOVE( FileRelationshipsWidget );
explicit FileRelationshipsWidget( idhan::hydrus::HydrusImporter* importer, QWidget* parent = nullptr );
private:
void updateText();
public slots:
void startImport();
void startPreImport();
void statusMessage( const QString& msg );
void processedDuplicates( std::size_t count );
void processedMaxDuplicates( std::size_t count );
void processedAlternatives( std::size_t count );
void processedMaxAlternatives( std::size_t count );
private:
Ui::FileRelationshipsWidget* ui;
};

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FileRelationshipsWidget</class>
<widget class="QWidget" name="FileRelationshipsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>907</width>
<height>98</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item alignment="Qt::AlignmentFlag::AlignTop">
<widget class="QLabel" name="name">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Name: Duplicate &amp; Alternative relationships
Type:File Relationships</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<item alignment="Qt::AlignmentFlag::AlignVCenter">
<widget class="QLabel" name="alternativesCount">
<property name="text">
<string>Alternative Groups: 0</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignmentFlag::AlignVCenter">
<widget class="QLabel" name="duplicatesCount">
<property name="text">
<string>Duplicate Pairs: 0</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(0, 170, 0);</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbShouldImport">
<property name="text">
<string>Import</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,200 @@
//
// Created by kj16609 on 11/5/25.
//
#include "FileRelationshipsWorker.hpp"
#include <moc_FileRelationshipsWorker.cpp>
#include "sqlitehelper/Query.hpp"
#include "sqlitehelper/TransactionBaseCoro.hpp"
FileRelationshipsWorker::FileRelationshipsWorker( QObject* parent, idhan::hydrus::HydrusImporter* importer ) :
QObject( parent ),
QRunnable(),
m_importer( importer )
{
this->setAutoDelete( false );
}
void FileRelationshipsWorker::preprocess()
{
idhan::hydrus::TransactionBaseCoro client_tr { m_importer->client_db };
idhan::hydrus::Query< int, int > duplicate_file_members {
client_tr,
"SELECT hash_id, king_hash_id FROM duplicate_file_members dfm JOIN duplicate_files df ON dfm.media_id = df.media_id WHERE hash_id != king_hash_id"
};
std::size_t duplicate_file_members_counter { 0 };
for ( [[maybe_unused]] const auto& [ media_id, hash_id ] : duplicate_file_members )
{
duplicate_file_members_counter++;
if ( duplicate_file_members_counter % 1000 == 0 ) emit processedMaxDuplicates( duplicate_file_members_counter );
}
emit processedMaxDuplicates( duplicate_file_members_counter );
idhan::hydrus::Query< int, int > alternative_file_members {
client_tr, "SELECT * FROM alternate_file_group_members JOIN duplicate_file_members USING (media_id)"
};
std::size_t alternative_file_members_counter { 0 };
for ( [[maybe_unused]] const auto& [ media_id, hash_id ] : alternative_file_members )
{
alternative_file_members_counter++;
if ( alternative_file_members_counter % 1000 == 0 )
emit processedMaxAlternatives( alternative_file_members_counter );
}
emit processedMaxAlternatives( alternative_file_members_counter );
}
void FileRelationshipsWorker::process()
{
idhan::hydrus::TransactionBaseCoro client_tr { m_importer->client_db };
using MediaID = std::uint32_t;
using HashID = std::uint32_t;
using KingID = std::uint32_t;
std::vector< HashID > hash_ids {};
std::unordered_map< HashID, idhan::RecordID > record_map {};
emit statusMessage( "Started" );
auto flushHashIDs = [ &, this ]()
{
emit statusMessage( "Mapping Hydrus IDs to IDHAN IDs" );
std::ranges::sort( hash_ids );
std::ranges::unique( hash_ids );
std::ranges::remove_if(
hash_ids, [ &record_map ]( const HashID hash_id ) -> bool { return record_map.contains( hash_id ); } );
if ( hash_ids.empty() ) return;
const auto batch_map { m_importer->mapHydrusRecords( hash_ids ) };
// merge the new map into the existing one
for ( const auto& [ hy_hash_id, idhan_hash_id ] : batch_map )
record_map.insert_or_assign( hy_hash_id, idhan_hash_id );
hash_ids.clear();
};
idhan::hydrus::Query< HashID, KingID > duplicate_files {
client_tr,
"SELECT hash_id, king_hash_id FROM duplicate_file_members dfm JOIN duplicate_files df ON dfm.media_id = df.media_id WHERE hash_id != king_hash_id"
};
std::vector< std::pair< HashID, KingID > > pairs {};
auto& client { idhan::IDHANClient::instance() };
std::size_t processed_count { 0 };
auto flushPairs = [ &, this ]()
{
flushHashIDs();
emit statusMessage( "Setting duplicates for batch" );
std::vector< std::pair< idhan::RecordID, idhan::RecordID > > idhan_pairs {};
for ( const auto& [ hy_hash_id, hy_king_id ] : pairs )
{
const auto idhan_hash_id { record_map.at( hy_hash_id ) };
const auto idhan_king_id { record_map.at( hy_king_id ) };
idhan_pairs.emplace_back( idhan_hash_id, idhan_king_id );
// auto future = client.setDuplicates( pairs );
// future.waitForFinished();
}
auto future { client.setDuplicates( idhan_pairs ) };
future.waitForFinished();
processed_count += pairs.size();
emit processedDuplicates( processed_count );
pairs.clear();
};
for ( const auto& [ hash_id, king_id ] : duplicate_files )
{
if ( hash_id == king_id ) continue;
if ( !record_map.contains( hash_id ) ) hash_ids.push_back( hash_id );
if ( !record_map.contains( king_id ) ) hash_ids.push_back( king_id );
pairs.emplace_back( std::make_pair( hash_id, king_id ) );
if ( pairs.size() >= 100 )
{
flushPairs();
emit statusMessage( "Getting additional rows to process" );
}
}
flushPairs();
idhan::hydrus::Query< HashID, MediaID > alternative_files {
client_tr,
"SELECT hash_id, alternates_group_id FROM alternate_file_group_members JOIN duplicate_file_members USING (media_id);"
};
using GroupID = std::uint32_t;
std::unordered_map< GroupID, std::vector< idhan::hydrus::HashID > > alternative_map {};
emit statusMessage( "Mapping alternative hashes to IDHAN" );
for ( const auto& [ hash_id, alternative_group_id ] : alternative_files )
{
hash_ids.push_back( hash_id );
if ( hash_ids.size() >= 64 ) flushHashIDs();
if ( auto itter = alternative_map.find( alternative_group_id ); itter != alternative_map.end() )
itter->second.push_back( hash_id );
else
alternative_map.emplace( alternative_group_id, std::vector< idhan::hydrus::HashID > { hash_id } );
}
flushHashIDs();
emit statusMessage( "Setting alternatives for groups" );
std::size_t alternative_count { 0 };
for ( const auto hy_hashes : alternative_map | std::views::values )
{
std::vector< idhan::RecordID > record_ids {};
for ( const auto& hy_hash : hy_hashes ) record_ids.emplace_back( record_map.at( hy_hash ) );
alternative_count += record_ids.size();
auto future = client.setAlternativeGroups( record_ids );
future.waitForFinished();
emit processedAlternatives( alternative_count );
}
emit statusMessage( "Finished" );
}
void FileRelationshipsWorker::run()
{
if ( !m_preprocessed )
{
m_preprocessed = true;
preprocess();
return;
}
process();
}

View File

@@ -0,0 +1,32 @@
//
// Created by kj16609 on 11/5/25.
//
#pragma once
#include <QObject>
#include "HydrusImporter.hpp"
class FileRelationshipsWorker : public QObject, public QRunnable
{
Q_OBJECT
idhan::hydrus::HydrusImporter* m_importer;
bool m_preprocessed { false };
signals:
void processedMaxDuplicates( std::size_t counter );
void processedMaxAlternatives( std::size_t counter );
void statusMessage( const QString& message );
void processedDuplicates( std::size_t );
void processedAlternatives( std::size_t );
public:
FileRelationshipsWorker( QObject* parent, idhan::hydrus::HydrusImporter* importer );
void preprocess();
void process();
void run() override;
};

View File

@@ -53,7 +53,7 @@ TagServiceWidget::~TagServiceWidget()
void TagServiceWidget::setName( const QString& name )
{
m_name = name;
ui->name->setText( QString( "Name: %L1" ).arg( name ) );
ui->name->setText( QString( "Name: %L1\nType: Tag Service" ).arg( name ) );
}
void TagServiceWidget::recordMappingProcessed( std::size_t count )
@@ -113,6 +113,12 @@ void TagServiceWidget::updateTime()
const std::size_t to_process { m_info.num_mappings + m_info.num_parents + m_info.num_aliases };
const std::size_t total_processed { mappings_processed + parents_processed + aliases_processed };
if ( total_processed == 0 )
{
ui->statusLabel->setText( "Ready!" );
return;
}
// const bool over_limit { to_process > std::numeric_limits< int >::max() };
// const std::size_t multip { over_limit ? 16 : 1 };
@@ -182,7 +188,7 @@ void TagServiceWidget::processedMappings( std::size_t count, std::size_t record_
QLocale locale { QLocale::English, QLocale::UnitedStates };
locale.setNumberOptions( QLocale::DefaultNumberOptions );
ui->mappingsCount->setText(
QString( "Mappings: %L1 (%L2 processed)\nRecords: (%L3 Records)" )
QString( "Mappings: %L1 (%L2 processed)\nRecords: (%L3 processed)" )
.arg( m_info.num_mappings )
.arg( mappings_processed )
.arg( records_processed ) );
@@ -222,7 +228,7 @@ void TagServiceWidget::setMaxMappings( std::size_t count )
m_info.num_mappings = count;
if ( mappings_processed > 0 )
ui->mappingsCount->setText(
QString( "Mappings: %L1 (%L2 processed)\nRecords: (%L3 Records)" )
QString( "Mappings: %L1 (%L2 processed)\nRecords: (%L3 processed)" )
.arg( m_info.num_mappings )
.arg( mappings_processed )
.arg( records_processed ) );

View File

@@ -9,7 +9,8 @@
#include <deque>
#include "HydrusImporter.hpp"
#include "HydrusImporterWidget.hpp"
class TagServiceWorker;
namespace Ui
{

View File

@@ -0,0 +1,213 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TagServiceWidget</class>
<widget class="QWidget" name="TagServiceWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>605</width>
<height>104</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="name">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Name: </string>
</property>
</widget>
</item>
<item alignment="Qt::AlignmentFlag::AlignBottom">
<widget class="QLabel" name="statusLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Preprocessing</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QLabel" name="mappingsCount">
<property name="text">
<string>Mappings: 0</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignmentFlag::AlignTop">
<widget class="QLabel" name="aliasesCount">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Aliases: 0</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<item alignment="Qt::AlignmentFlag::AlignTop">
<widget class="QLabel" name="parentsCount">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Parents/Children: 0</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignBottom">
<widget class="QCheckBox" name="checkBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Only processes mappings for files that you've obtained&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Only acquired files</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(0, 170, 0);</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbShouldImport">
<property name="text">
<string>Import</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -4,12 +4,13 @@
#include "TagServiceWorker.hpp"
#include <moc_TagServiceWorker.cpp>
#include <QObject>
#include <QtConcurrent>
#include <fstream>
#include "moc_TagServiceWorker.cpp"
#include "sqlitehelper/Query.hpp"
#include "sqlitehelper/Transaction.hpp"
#include "sqlitehelper/TransactionBaseCoro.hpp"

View File

@@ -0,0 +1,58 @@
//
// Created by kj16609 on 11/7/25.
//
// You may need to build the project (run Qt uic code generator) to get "ui_UrlServiceWidget.h" resolved
#include "UrlServiceWidget.hpp"
#include <QThreadPool>
#include "UrlServiceWorker.hpp"
#include "ui_UrlServiceWidget.h"
UrlServiceWidget::UrlServiceWidget( idhan::hydrus::HydrusImporter* get, QWidget* parent ) :
QWidget( parent ),
ui( new Ui::UrlServiceWidget() ),
m_importer( get )
{
ui->setupUi( this );
m_worker = new UrlServiceWorker( this, m_importer );
connect( m_worker, &UrlServiceWorker::processedMaxUrls, this, &UrlServiceWidget::processedMaxUrls );
connect( m_worker, &UrlServiceWorker::processedUrls, this, &UrlServiceWidget::processedUrls );
connect( m_worker, &UrlServiceWorker::statusMessage, this, &UrlServiceWidget::statusMessage );
}
UrlServiceWidget::~UrlServiceWidget()
{
delete ui;
}
void UrlServiceWidget::startPreImport()
{
QThreadPool::globalInstance()->start( m_worker );
}
void UrlServiceWidget::startImport()
{
if ( ui->cbShouldImport->isChecked() ) QThreadPool::globalInstance()->start( m_worker );
}
void UrlServiceWidget::statusMessage( const QString& msg )
{
ui->statusLabel->setText( msg );
}
void UrlServiceWidget::processedMaxUrls( const std::size_t count )
{
ui->urlCount->setText( QString( "URLs: %L1" ).arg( count ) );
ui->progressBar->setMaximum( static_cast< int >( count ) );
m_max_urls = count;
}
void UrlServiceWidget::processedUrls( const std::size_t count )
{
ui->urlCount->setText( QString( "URLs: %L1 (%L2 processed)" ).arg( m_max_urls ).arg( count ) );
ui->progressBar->setValue( static_cast< int >( count ) );
}

View File

@@ -0,0 +1,47 @@
//
// Created by kj16609 on 11/7/25.
//
#ifndef IDHAN_URLSERVICEWIDGET_HPP
#define IDHAN_URLSERVICEWIDGET_HPP
#include <QWidget>
class UrlServiceWorker;
namespace idhan::hydrus
{
class HydrusImporter;
}
QT_BEGIN_NAMESPACE
namespace Ui
{
class UrlServiceWidget;
}
QT_END_NAMESPACE
class UrlServiceWidget : public QWidget
{
Q_OBJECT
Ui::UrlServiceWidget* ui;
idhan::hydrus::HydrusImporter* m_importer;
UrlServiceWorker* m_worker { nullptr };
std::size_t m_max_urls { 0 };
public:
explicit UrlServiceWidget( idhan::hydrus::HydrusImporter* get, QWidget* parent = nullptr );
~UrlServiceWidget() override;
public slots:
void startPreImport();
void startImport();
void statusMessage( const QString& msg );
void processedMaxUrls( std::size_t count );
void processedUrls( std::size_t count );
};
#endif //IDHAN_URLSERVICEWIDGET_HPP

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>UrlServiceWidget</class>
<widget class="QWidget" name="UrlServiceWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>543</width>
<height>106</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item alignment="Qt::AlignmentFlag::AlignTop">
<widget class="QLabel" name="name">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Name: Record URLs</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignmentFlag::AlignBottom">
<widget class="QLabel" name="statusLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<item alignment="Qt::AlignmentFlag::AlignTop">
<widget class="QLabel" name="urlCount">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>URLs: 0</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(0, 170, 0);</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbShouldImport">
<property name="text">
<string>Import</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,105 @@
//
// Created by kj16609 on 11/7/25.
//
#include "UrlServiceWorker.hpp"
#include <moc_UrlServiceWorker.cpp>
#include "sqlitehelper/Query.hpp"
#include "sqlitehelper/TransactionBaseCoro.hpp"
UrlServiceWorker::UrlServiceWorker( QObject* parent, idhan::hydrus::HydrusImporter* importer ) :
QObject( parent ),
QRunnable(),
m_importer( importer )
{
this->setAutoDelete( false );
}
void UrlServiceWorker::preprocess()
{
idhan::hydrus::TransactionBaseCoro client_tr { m_importer->client_db };
std::size_t url_counter { 0 };
idhan::hydrus::Query< int, int > query { client_tr, "SELECT hash_id, url_id FROM url_map" };
for ( [[maybe_unused]] const auto& [ hash_id, url_id ] : query )
{
url_counter += 1;
if ( url_counter % 10'000 == 0 ) emit processedMaxUrls( url_counter );
}
emit processedMaxUrls( url_counter );
}
void UrlServiceWorker::process()
{
auto& client { idhan::IDHANClient::instance() };
idhan::hydrus::TransactionBaseCoro client_tr { m_importer->client_db };
idhan::hydrus::TransactionBaseCoro master_tr { m_importer->master_db };
std::size_t url_counter { 0 };
idhan::hydrus::Query< int, int > query { client_tr, "SELECT hash_id, url_id FROM url_map ORDER BY hash_id ASC" };
std::unordered_map< idhan::hydrus::HashID, std::vector< std::string > > current_urls {};
auto flushUrls = [ &, this ]()
{
emit statusMessage( "Mapping hydrus IDs to IDHAN IDs" );
std::vector< idhan::hydrus::HashID > hashes {};
for ( const auto& hash_id : current_urls | std::views::keys )
{
hashes.emplace_back( hash_id );
}
const auto mapped_ids { m_importer->mapHydrusRecords( hashes ) };
for ( const auto& [ hash_id, idhan_id ] : mapped_ids )
{
auto urls { current_urls[ hash_id ] };
auto future { client.addUrls( idhan_id, urls ) };
future.waitForFinished();
}
current_urls.clear();
emit processedUrls( url_counter );
};
for ( [[maybe_unused]] const auto& [ hash_id, url_id ] : query )
{
idhan::hydrus::Query< std::string_view > url_query {
master_tr, "SELECT url FROM urls WHERE url_id = $1", url_id
};
std::vector< std::string > urls {};
for ( const auto& [ url ] : url_query )
{
urls.emplace_back( url );
}
url_counter += urls.size();
current_urls.emplace( hash_id, std::move( urls ) );
if ( url_counter % 500 == 0 ) flushUrls();
}
flushUrls();
emit statusMessage( "Finished!" );
}
void UrlServiceWorker::run()
{
if ( !m_preprocessed )
{
m_preprocessed = true;
preprocess();
return;
}
process();
}

View File

@@ -0,0 +1,29 @@
//
// Created by kj16609 on 11/7/25.
//
#pragma once
#include <QObject>
#include "HydrusImporter.hpp"
class UrlServiceWorker : public QObject, public QRunnable
{
Q_OBJECT
idhan::hydrus::HydrusImporter* m_importer;
bool m_preprocessed { false };
signals:
void processedMaxUrls( std::size_t counter );
void processedUrls( std::size_t counter );
void statusMessage( const QString& message );
public:
UrlServiceWorker( QObject* parent, idhan::hydrus::HydrusImporter* importer );
void preprocess();
void process();
void run() override;
};

View File

@@ -5,14 +5,16 @@
#include "MainWindow.hpp"
#include <QFutureWatcher>
#include <moc_MainWindow.cpp>
#include <QTimer>
#include <idhan/IDHANClient.hpp>
#include "../hydrus/HydrusImporterWidget.hpp"
#include "NET_CONSTANTS.hpp"
#include "SettingsDialog.hpp"
#include "gui/hydrus/HydrusImporterWidget.hpp"
#include "gui/hydrus/tag_service/TagServiceWidget.hpp"
#include "ui_MainWindow.h"
MainWindow::MainWindow( QWidget* parent ) :
@@ -96,10 +98,11 @@ void MainWindow::checkHeartbeat()
{
const auto result { watcher->result() };
ui->statusbar->showMessage( QString( "Connected to IDHAN v%1 (Build: %4, Commit: %5)" )
.arg( result.server.str )
.arg( result.build_type )
.arg( result.commit ) );
ui->statusbar->showMessage(
QString( "Connected to IDHAN v%1 (Build: %4, Commit: %5)" )
.arg( result.server.str )
.arg( result.build_type )
.arg( result.commit ) );
}
catch ( std::exception& e )
{

View File

@@ -4,6 +4,8 @@
#include "SettingsDialog.hpp"
#include <moc_SettingsDialog.cpp>
#include <QFutureWatcher>
#include "idhan/IDHANClient.hpp"
@@ -41,10 +43,11 @@ void SettingsDialog::on_testConnection_pressed()
{
const auto result { watcher->result() };
ui->networkSettingsLabel->setText( QString( "Connected to IDHAN v%1 (Build: %4, Commit: %5)" )
.arg( result.server.str )
.arg( result.build_type )
.arg( result.commit ) );
ui->networkSettingsLabel->setText(
QString( "Connected to IDHAN v%1 (Build: %4, Commit: %5)" )
.arg( result.server.str )
.arg( result.build_type )
.arg( result.commit ) );
}
catch ( std::exception& e )
{

View File

@@ -40,22 +40,23 @@ set(GENERATED_HEADERS "")
file(MAKE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/hydrus)
set(COMMAND_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake_modules/GenerateHydrusConstants.cmake")
foreach (HYDRUS_FILE ${HYDRUS_SCAN_FILES})
get_filename_component(FILE_NAME ${HYDRUS_FILE} NAME_WE)
set(OUTPUT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/include/hydrus/${FILE_NAME}_gen.hpp")
list(APPEND GENERATED_HEADERS ${OUTPUT_FILE})
add_custom_target(
generate_${FILE_NAME}_constants ALL
add_custom_command(
OUTPUT ${OUTPUT_FILE}
COMMAND ${CMAKE_COMMAND}
-DHYDRUS_DIR=${HYDRUS_REPO_PATH}
-DHYDRUS_CONSTANTS_FILE=${HYDRUS_FILE}
-DOUT_TARGET=${OUTPUT_FILE}
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake_modules/GenerateHydrusConstants.cmake"
-P ${COMMAND_FILE}
DEPENDS ${HYDRUS_FILE} ${COMMAND_FILE}
COMMENT "Generating ${FILE_NAME} constants file"
)
add_dependencies(IDHAN generate_${FILE_NAME}_constants)
endforeach ()
target_sources(IDHAN PUBLIC ${GENERATED_HEADERS})

View File

@@ -0,0 +1,24 @@
//
// Created by kj16609 on 7/30/24.
//
#pragma once
#define MAKE_IDHAN_VERSION( major, minor, patch ) int( ( major << 16 ) | ( minor < 8 ) | patch )
#ifndef IDHAN_MAJOR_VERSION
#error Major version must be specified for release builds
#endif
#ifndef IDHAN_MINOR_VERSION
#error Minor version must be specified for release builds
#endif
#ifndef IDHAN_PATCH_VERSION
#error Patch version must be specified for release builds
#endif
#define IDHAN_VERSION MAKE_IDHAN_VERSION( IDHAN_MAJOR_VERSION, IDHAN_MINOR_VERSION, IDHAN_PATCH_VERSION )
#define IDHAN_BUILD_TIME __TIME__
#define IDHAN_BUILD_DATE __DATE__

View File

@@ -143,6 +143,18 @@ class IDHANClient
TagDomainID tag_domain_id,
std::vector< std::vector< std::pair< std::string, std::string > > >&& tag_sets );
// File relationships
QFuture< void > setAlternativeGroups( std::vector< RecordID >& record_ids );
QFuture< void > setDuplicates( RecordID worse_duplicate, RecordID better_duplicate );
/**
*
* @param pairs Pairs of ids in a (worse_id, better_id) format
* @return
*/
QFuture< void > setDuplicates( const std::vector< std::pair< RecordID, RecordID > >& pairs );
/**
* @brief Creates a parent/child relationship between two tags
* @param parent_id
@@ -238,6 +250,14 @@ class IDHANClient
std::uint16_t ratio,
bool readonly );
QFuture< void > addUrls( RecordID record_id, const std::vector< std::string >& urls );
inline QFuture< void > addUrl( const RecordID record_id, std::string url )
{
std::vector< std::string > urls { { url } };
return addUrls( record_id, urls );
}
private:
void sendClientGet( UrlVariant url, IDHANResponseHandler&& responseHandler, IDHANErrorHandler&& errorHandler );

View File

@@ -200,68 +200,70 @@ void IDHANClient::sendClientJson(
const auto submit_time { std::chrono::high_resolution_clock::now() };
QObject::connect(
response,
&QNetworkReply::finished,
[ responseHandler, response, submit_time ]()
auto responseSlot = [ responseHandler, response, submit_time ]()
{
const auto response_in_time { std::chrono::high_resolution_clock::now() };
if ( const auto response_time = response_in_time - submit_time; response_time > std::chrono::seconds( 5 ) )
{
const auto response_in_time { std::chrono::high_resolution_clock::now() };
logging::warn(
"Server took {}ms to response to query {}. Might be doing a lot of work?",
std::chrono::duration_cast< std::chrono::milliseconds >( response_time ).count(),
response->url().path().toStdString() );
}
if ( const auto response_time = response_in_time - submit_time; response_time > std::chrono::seconds( 5 ) )
{
logging::warn(
"Server took {}ms to response to query {}. Might be doing a lot of work?",
std::chrono::duration_cast< std::chrono::milliseconds >( response_time ).count(),
response->url().path().toStdString() );
}
if ( response->error() != QNetworkReply::NoError ) return;
if ( response->error() != QNetworkReply::NoError ) return;
QThreadPool::globalInstance()->start( std::bind( responseHandler, response ) );
};
QThreadPool::globalInstance()->start( std::bind( responseHandler, response ) );
// responseHandler( response );
// response->deleteLater();
} );
QObject::connect(
response,
&QNetworkReply::errorOccurred,
[ errorHandler, response, url ]( const QNetworkReply::NetworkError error )
auto errorSlot = [ errorHandler, response, url ]( const QNetworkReply::NetworkError error )
{
if ( error == QNetworkReply::NetworkError::OperationCanceledError )
{
if ( error == QNetworkReply::NetworkError::OperationCanceledError )
{
logging::critical(
"Operation timed out with request to {}: {}",
url.toString(),
response->errorString().toStdString() );
std::abort();
}
logging::critical(
"Operation timed out with request to {}: {}", url.toString(), response->errorString().toStdString() );
std::abort();
}
// check if this is a special error or not.
// It should have json if so
auto header = response->header( QNetworkRequest::ContentTypeHeader );
if ( header.isValid() && header.toString().contains( "application/json" ) )
// check if this is a special error or not.
// It should have json if so
auto header = response->header( QNetworkRequest::ContentTypeHeader );
if ( header.isValid() && header.toString().contains( "application/json" ) )
{
const auto response_body { response->readAll() };
QJsonDocument response_doc { QJsonDocument::fromJson( response_body ) };
if ( response_doc.isObject() )
{
const auto response_body { response->readAll() };
QJsonDocument response_doc { QJsonDocument::fromJson( response_body ) };
if ( response_doc.isObject() )
QJsonObject response_object { response_doc.object() };
if ( response_object.contains( "error" ) )
{
QJsonObject response_object { response_doc.object() };
if ( response_object.contains( "error" ) )
{
const auto error_msg { response_object[ "error" ].toString().toStdString() };
// logging::error( object[ "error" ].toString().toStdString() );
const auto error_msg { response_object[ "error" ].toString().toStdString() };
// logging::error( object[ "error" ].toString().toStdString() );
QThreadPool::globalInstance()->start( std::bind( errorHandler, response, error, error_msg ) );
return;
}
QThreadPool::globalInstance()->start( std::bind( errorHandler, response, error, error_msg ) );
return;
}
}
}
QThreadPool::globalInstance()->start(
std::bind( errorHandler, response, error, response->errorString().toStdString() ) );
// errorHandler( response, error );
// response->deleteLater();
} );
QThreadPool::globalInstance()->start(
std::bind( errorHandler, response, error, response->errorString().toStdString() ) );
};
const auto response_connection { QObject::connect( response, &QNetworkReply::finished, responseSlot ) };
const auto error_connection { QObject::connect( response, &QNetworkReply::errorOccurred, errorSlot ) };
if ( response->isFinished() )
{
response->disconnect( response_connection );
response->disconnect( error_connection );
if ( response->error() == QNetworkReply::NoError )
responseSlot();
else
errorSlot( response->error() );
}
}
QFuture< void > IDHANClient::createFileCluster(

View File

@@ -0,0 +1,54 @@
//
// Created by kj16609 on 11/8/25.
//
#include <QFuture>
#include <QJsonArray>
#include <QJsonObject>
#include "idhan/IDHANClient.hpp"
namespace idhan
{
QFuture< void > IDHANClient::addUrls( const RecordID record_id, const std::vector< std::string >& urls )
{
if ( urls.empty() ) return QtFuture::makeReadyVoidFuture();
QJsonObject json {};
QJsonArray array {};
for ( const auto& url : urls )
{
array.append( QString::fromStdString( url ) );
}
json[ "urls" ] = array;
auto promise = std::make_shared< QPromise< void > >();
const auto url { format_ns::format( "/records/{}/urls/add", record_id ) };
auto handleResponse = [ promise ]( QNetworkReply* response )
{
const auto data = response->readAll();
if ( !response->isFinished() )
{
logging::info( "Failed to read response" );
throw std::runtime_error( "Failed to read response" );
}
promise->finish();
response->deleteLater();
};
QJsonDocument doc {};
doc.setObject( json );
sendClientPost( std::move( doc ), url.data(), handleResponse, defaultErrorHandler( promise ) );
return promise->future();
}
} // namespace idhan

View File

@@ -0,0 +1,46 @@
//
// Created by kj16609 on 11/10/25.
//
#include <QFuture>
#include <QJsonArray>
#include <QJsonObject>
#include "IDHANTypes.hpp"
#include "idhan/IDHANClient.hpp"
namespace idhan
{
QFuture< void > IDHANClient::setAlternativeGroups( std::vector< RecordID >& record_ids )
{
if ( record_ids.empty() ) return QtFuture::makeReadyVoidFuture();
QJsonArray record_array {};
for ( const auto& record : record_ids ) record_array.append( record );
auto promise { std::make_shared< QPromise< void > >() };
auto handleResponse = [ promise ]( QNetworkReply* reply ) -> void
{
const auto data = reply->readAll();
if ( !reply->isFinished() )
{
logging::info( "Failed to read response" );
throw std::runtime_error( "Failed to read response" );
}
promise->finish();
reply->deleteLater();
};
QJsonDocument doc {};
doc.setArray( record_array );
sendClientPost(
std::move( doc ), "/relationships/alternatives/add", handleResponse, defaultErrorHandler( promise ) );
return promise->future();
}
} // namespace idhan

View File

@@ -0,0 +1,57 @@
//
// Created by kj16609 on 11/5/25.
//
#include <QFuture>
#include <QJsonArray>
#include <QJsonObject>
#include "IDHANTypes.hpp"
#include "idhan/IDHANClient.hpp"
namespace idhan
{
QFuture< void > IDHANClient::setDuplicates( const RecordID worse_duplicate, const RecordID better_duplicate )
{
const std::vector< std::pair< RecordID, RecordID > > duplicates { { worse_duplicate, better_duplicate } };
return setDuplicates( duplicates );
}
QFuture< void > IDHANClient::setDuplicates( const std::vector< std::pair< RecordID, RecordID > >& pairs )
{
QJsonArray json_pairs {};
for ( const auto& [ worse_id, better_id ] : pairs )
{
QJsonObject json_pair {};
json_pair[ "worse_id" ] = worse_id;
json_pair[ "better_id" ] = better_id;
json_pairs.append( json_pair );
}
auto promise { std::make_shared< QPromise< void > >() };
auto handleResponse = [ promise ]( QNetworkReply* response ) -> void
{
const auto data = response->readAll();
if ( !response->isFinished() )
{
logging::info( "Failed to read response" );
throw std::runtime_error( "Failed to read response" );
}
promise->finish();
response->deleteLater();
};
QJsonDocument doc {};
doc.setArray( std::move( json_pairs ) );
sendClientPost( std::move( doc ), "/relationships/duplicates/add", handleResponse, defaultErrorHandler( promise ) );
return promise->future();
}
} // namespace idhan

View File

@@ -0,0 +1,53 @@
CREATE TABLE alternative_groups
(
group_id SERIAL PRIMARY KEY NOT NULL
);
CREATE TABLE alternative_group_members
(
group_id INTEGER REFERENCES alternative_groups (group_id) NOT NULL,
record_id INTEGER REFERENCES records (record_id) UNIQUE NOT NULL,
UNIQUE (group_id, record_id)
);
CREATE INDEX ON alternative_group_members (group_id);
CREATE TABLE duplicate_pairs
(
worse_record_id INTEGER REFERENCES records (record_id) UNIQUE NOT NULL,
better_record_id INTEGER REFERENCES records (record_id) NOT NULL,
CHECK (worse_record_id != better_record_id)
);
CREATE INDEX ON duplicate_pairs (better_record_id);
CREATE OR REPLACE FUNCTION insert_duplicate_pair(
worse INTEGER,
better INTEGER
) RETURNS VOID AS
$$
BEGIN
IF (EXISTS(SELECT 1 FROM duplicate_pairs WHERE worse_record_id = worse AND better_record_id = better)) THEN
RETURN;
END IF;
-- check that the worse id isn't being added again
IF (EXISTS(SELECT 1 FROM duplicate_pairs WHERE worse_record_id = worse)) THEN
RAISE EXCEPTION 'Can''t insert an already inserted worse record';
END IF;
-- Check that the worse record wouldn't make a cyclic chain
IF (EXISTS(SELECT 1 FROM duplicate_pairs WHERE better_record_id = worse AND worse_record_id = better)) THEN
RAISE EXCEPTION 'Inserting this duplicate pair would result in a cyclic chain';
end if;
INSERT INTO duplicate_pairs (worse_record_id, better_record_id)
VALUES (worse, better);
UPDATE duplicate_pairs
SET better_record_id = better
WHERE better_record_id = worse;
END;
$$ LANGUAGE plpgsql;

View File

@@ -2,7 +2,6 @@ project(IDHANServer LANGUAGES CXX C)
AddFGLExecutable(IDHANServer ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_sources(IDHANServer PRIVATE ${MIGRATION_SOURCE})
# Gui is needed for QImage for whatever reason

View File

@@ -58,8 +58,14 @@ void ServerContext::setupCORSSupport() const
} );
drogon::app().registerPostHandlingAdvice(
[]( [[maybe_unused]] const drogon::HttpRequestPtr& request, const drogon::HttpResponsePtr& response )
{ addCORSHeaders( response ); } );
[ this ]( [[maybe_unused]] const drogon::HttpRequestPtr& request, const drogon::HttpResponsePtr& response )
{
if ( args.testmode )
log::info( "Finished Handling query: {}:{}", request->getMethodString(), request->getPath() );
else
log::debug( "Finished Handling query: {}:{}", request->getMethodString(), request->getPath() );
addCORSHeaders( response );
} );
}
void exceptionHandler( const std::exception& e, const drogon::HttpRequestPtr& request, ResponseFunction&& callback )

View File

@@ -0,0 +1,26 @@
//
// Created by kj16609 on 11/5/25.
//
#pragma once
#include "drogon/HttpController.h"
namespace idhan::api
{
class FileRelationshipsAPI : public drogon::HttpController< FileRelationshipsAPI >
{
drogon::Task< drogon::HttpResponsePtr > setBetterDuplicate( drogon::HttpRequestPtr request );
drogon::Task< drogon::HttpResponsePtr > addAlternative( drogon::HttpRequestPtr request );
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO( FileRelationshipsAPI::setBetterDuplicate, "/relationships/duplicates/add" );
ADD_METHOD_TO( FileRelationshipsAPI::addAlternative, "/relationships/alternatives/add" );
METHOD_LIST_END
};
} // namespace idhan::api

View File

@@ -8,30 +8,55 @@
namespace idhan::helpers
{
drogon::Task< std::expected< UrlID, drogon::HttpResponsePtr > > findOrCreateUrl( const std::string url, DbClientPtr db )
ExpectedTask< UrlDomainID > findOrCreateUrl( const std::string url, DbClientPtr db )
{
UrlID url_id { INVALID_URL_ID };
std::size_t tries { 0 };
const auto search_result { co_await db->execSqlCoro( "SELECT url_id FROM urls WHERE url = $1", url ) };
do
{
tries += 1;
if ( tries > 16 ) co_return std::unexpected( createBadRequest( "Too many URL creation attempts" ) );
const auto search_result { co_await db->execSqlCoro( "SELECT url_id FROM urls WHERE url = $1", url ) };
if ( !search_result.empty() ) [[unlikely]]
co_return search_result[ 0 ][ 0 ].as< UrlID >();
if ( !search_result.empty() )
{
url_id = search_result[ 0 ][ 0 ].as< UrlID >();
break;
}
const auto url_domain_id { co_await helpers::findOrCreateUrlDomain( url, db ) };
return_unexpected_error( url_domain_id );
const auto insert { co_await db->execSqlCoro(
"INSERT INTO urls (url) VALUES ($1) ON CONFLICT DO NOTHING RETURNING url_id", url ) };
const auto insert { co_await db->execSqlCoro(
"INSERT INTO urls (url, url_domain_id) VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING url_id",
url,
*url_domain_id ) };
if ( !insert.empty() ) url_id = insert[ 0 ][ 0 ].as< UrlID >();
}
while ( url_id == INVALID_URL_ID );
if ( insert.empty() ) [[unlikely]]
co_return std::unexpected( createInternalError( "Was unable to create or find url {}", url ) );
co_return url_id;
co_return insert[ 0 ][ 0 ].as< UrlID >();
}
ExpectedTask< UrlDomainID > findOrCreateUrlDomain( const std::string url, DbClientPtr db )
{
// Extract domain from URL (netloc/host part)
const auto protocol_end = url.find( "://" );
const auto start_pos = protocol_end != std::string::npos ? protocol_end + 3 : 0;
const auto path_start = url.find( '/', start_pos );
std::string domain {
path_start != std::string::npos ? url.substr( start_pos, path_start - start_pos ) : url.substr( start_pos )
};
// Remove port if present
const auto port_pos = domain.find( ':' );
if ( port_pos != std::string::npos ) domain = domain.substr( 0, port_pos );
const auto search_result {
co_await db->execSqlCoro( "SELECT url_domain_id FROM url_domains WHERE url_domain = $1", domain )
};
if ( !search_result.empty() ) [[unlikely]]
co_return search_result[ 0 ][ 0 ].as< UrlDomainID >();
const auto insert { co_await db->execSqlCoro(
"INSERT INTO url_domains (url_domain) VALUES ($1) ON CONFLICT DO NOTHING RETURNING url_domain_id", domain ) };
if ( insert.empty() ) [[unlikely]]
co_return std::unexpected( createInternalError( "Failed to create URL domain" ) );
co_return insert[ 0 ][ 0 ].as< UrlDomainID >();
}
} // namespace idhan::helpers

View File

@@ -4,11 +4,11 @@
#pragma once
#include <drogon/HttpResponse.h>
#include <drogon/orm/DbClient.h>
#include <expected>
#include <string>
#include "ExpectedTask.hpp"
#include "IDHANTypes.hpp"
#include "db/dbTypes.hpp"
@@ -16,5 +16,7 @@ namespace idhan::helpers
{
constexpr UrlID INVALID_URL_ID { 0 };
drogon::Task< std::expected< UrlID, drogon::HttpResponsePtr > > findOrCreateUrl( std::string url, DbClientPtr db );
ExpectedTask< UrlID > findOrCreateUrl( std::string url, DbClientPtr db );
ExpectedTask< UrlDomainID > findOrCreateUrlDomain( std::string url, DbClientPtr db );
} // namespace idhan::helpers

View File

@@ -30,10 +30,13 @@ drogon::Task< drogon::HttpResponsePtr > RecordAPI::addUrls( drogon::HttpRequestP
if ( !url_id ) co_return url_id.error();
co_await db->execSqlCoro(
"INSERT INTO url_mappings (url_id, record_id) VALUES ($1, $2)", url_id.value(), record_id );
"INSERT INTO url_mappings (url_id, record_id) VALUES ($1, $2) ON CONFLICT DO NOTHING", *url_id, record_id );
}
co_return drogon::HttpResponse::newHttpResponse();
Json::Value result {};
result[ "status" ] = drogon::k200OK;
co_return drogon::HttpResponse::newHttpJsonResponse( result );
}
} // namespace idhan::api

View File

@@ -0,0 +1,124 @@
//
// Created by kj16609 on 11/5/25.
//
#include "IDHANTypes.hpp"
#include "api/FileRelationshipsAPI.hpp"
#include "api/helpers/createBadRequest.hpp"
#include "db/drogonArrayBind.hpp"
namespace idhan::api
{
using GroupID = idhan::Integer;
drogon::Task< GroupID > createNewGroup( DbClientPtr db )
{
const auto group_result {
co_await db->execSqlCoro( "INSERT INTO alternative_groups DEFAULT VALUES RETURNING group_id" )
};
const auto group_id { group_result[ 0 ][ 0 ].as< GroupID >() };
co_return group_id;
}
drogon::Task<> addItemsToNewGroup( std::vector< RecordID > record_ids, DbClientPtr db )
{
const auto group_id { co_await createNewGroup( db ) };
co_await db->execSqlCoro(
"INSERT INTO alternative_group_members (group_id, record_id) VALUES ($1, UNNEST($2::" RECORD_PG_TYPE_NAME
"[]))",
group_id,
std::move( record_ids ) );
}
drogon::Task<> addItemsToExistingGroup(
std::vector< RecordID > record_ids,
const std::vector< GroupID > group_ids,
drogon::orm::DbClientPtr db )
{
const auto group_id { group_ids[ 0 ] };
co_await db->execSqlCoro(
"INSERT INTO alternative_group_members (group_id, record_id) VALUES ($1, UNNEST($2::" RECORD_PG_TYPE_NAME
"[])) ON CONFLICT (group_id, record_id) DO NOTHING",
group_id,
std::move( record_ids ) );
}
drogon::Task<> addItemsToExistingGroupsMerge(
std::vector< RecordID > record_ids,
std::vector< GroupID > group_ids,
drogon::orm::DbClientPtr db )
{
const auto group_id { co_await createNewGroup( db ) };
co_await db->execSqlCoro(
"UPDATE alternative_group_members SET group_id = $1 WHERE group_id = ANY($2)",
group_id,
std::forward< std::vector< GroupID > >( group_ids ) );
co_await db->execSqlCoro(
"INSERT INTO alternative_group_members (group_id, record_id) VALUES ($1, UNNEST($2::" RECORD_PG_TYPE_NAME
"[])) ON CONFLICT (group_id, record_id) DO NOTHING",
group_id,
std::forward< std::vector< RecordID > >( record_ids ) );
co_await db->execSqlCoro(
"DELETE FROM alternative_groups WHERE group_id = ANY($1)",
std::forward< std::vector< GroupID > >( group_ids ) );
}
drogon::Task< drogon::HttpResponsePtr > FileRelationshipsAPI::addAlternative( drogon::HttpRequestPtr request )
{
auto db { drogon::app().getDbClient() };
const auto json_ptr { request->getJsonObject() };
if ( !json_ptr ) co_return createBadRequest( "Expected json body" );
const auto& json { *json_ptr };
if ( !json.isArray() ) co_return createBadRequest( "Expected json array of integers" );
std::vector< RecordID > record_ids {};
for ( const auto& id : json )
{
record_ids.emplace_back( id.as< RecordID >() );
}
const auto group_search { co_await db->execSqlCoro(
"SELECT DISTINCT group_id FROM alternative_group_members WHERE record_id = ANY($1)",
std::forward< std::vector< RecordID > >( record_ids ) ) };
std::vector< GroupID > group_ids {};
for ( const auto& id : group_search )
{
group_ids.emplace_back( id[ 0 ].as< GroupID >() );
}
std::ranges::sort( group_ids );
std::ranges::unique( group_ids );
const bool no_existing_groups { group_ids.empty() };
const bool existing_groups { not no_existing_groups };
const bool multiple_groups { not no_existing_groups && group_ids.size() > 1 };
if ( no_existing_groups )
{
co_await addItemsToNewGroup( record_ids, db );
}
else if ( existing_groups && !multiple_groups )
{
co_await addItemsToExistingGroup( record_ids, group_ids, db );
}
else if ( multiple_groups )
{
co_await addItemsToExistingGroupsMerge( record_ids, group_ids, db );
}
co_return drogon::HttpResponse::newHttpJsonResponse( {} );
}
} // namespace idhan::api

View File

@@ -0,0 +1,61 @@
//
// Created by kj16609 on 11/5/25.
//
#include "IDHANTypes.hpp"
#include "api/FileRelationshipsAPI.hpp"
#include "api/helpers/createBadRequest.hpp"
namespace idhan::api
{
drogon::Task< drogon::HttpResponsePtr > setBetterDuplicateMultiple( const Json::Value json )
{
auto db { drogon::app().getDbClient() };
for ( const auto& object : json )
{
if ( !object.isMember( "worse_id" ) || !object.isMember( "better_id" ) )
co_return createBadRequest( "Expected object to have `worse_id` and `better_id`" );
if ( !object[ "worse_id" ].isUInt() || !object[ "better_id" ].isUInt() )
co_return createBadRequest( "Expected `worse_id` and `better_id` to be unsigned integers" );
const RecordID worse_id { object[ "worse_id" ].as< RecordID >() };
const RecordID better_id { object[ "better_id" ].as< RecordID >() };
co_await db->execSqlCoro( "SELECT insert_duplicate_pair($1, $2)", worse_id, better_id );
}
co_return drogon::HttpResponse::newHttpJsonResponse( {} );
}
drogon::Task< drogon::HttpResponsePtr > setBetterDuplicateSingle( const Json::Value json )
{
auto db { drogon::app().getDbClient() };
if ( !json[ "worse_id" ].isUInt() || !json[ "better_id" ].isUInt() )
co_return createBadRequest( "Expected `worse_id` and `better_id` to be unsigned integers" );
const RecordID worse_id { json[ "worse_id" ].as< RecordID >() };
const RecordID better_id { json[ "better_id" ].as< RecordID >() };
co_await db->execSqlCoro( "SELECT insert_duplicate_pair($1, $2)", worse_id, better_id );
co_return drogon::HttpResponse::newHttpJsonResponse( {} );
}
drogon::Task< drogon::HttpResponsePtr > FileRelationshipsAPI::setBetterDuplicate( const drogon::HttpRequestPtr request )
{
const auto json_ptr { request->getJsonObject() };
if ( !json_ptr ) co_return createBadRequest( "Expected json body" );
const auto& json { *json_ptr };
if ( json.isArray() ) co_return co_await setBetterDuplicateMultiple( json );
if ( json.isObject() ) co_return co_await setBetterDuplicateSingle( json );
co_return createBadRequest(
"Expected json body of either array of objects, or a single object. Objects must have `better_id` and `worse_id`" );
}
} // namespace idhan::api

View File

@@ -2,12 +2,14 @@
// Created by kj16609 on 11/8/24.
//
#include "version.hpp"
#include <paths.hpp>
#include "InfoAPI.hpp"
#include "hyapi/constants/hydrus_version.hpp"
#include "idhan/versions.hpp"
#include "logging/log.hpp"
#include "versions.hpp"
namespace idhan::api
{
@@ -19,10 +21,10 @@ drogon::Task< drogon::HttpResponsePtr > InfoAPI::version( [[maybe_unused]] drogo
Json::Value json;
json[ "idhan_server_version" ][ "string" ] =
format_ns::format( "{}.{}.{}", IDHAN_SERVER_MAJOR, IDHAN_SERVER_MINOR, IDHAN_SERVER_PATCH );
json[ "idhan_server_version" ][ "major" ] = IDHAN_SERVER_MAJOR;
json[ "idhan_server_version" ][ "minor" ] = IDHAN_SERVER_MINOR;
json[ "idhan_server_version" ][ "patch" ] = IDHAN_SERVER_PATCH;
format_ns::format( "{}.{}.{}", IDHAN_MAJOR_VERSION, IDHAN_MINOR_VERSION, IDHAN_PATCH_VERSION );
json[ "idhan_server_version" ][ "major" ] = IDHAN_MAJOR_VERSION;
json[ "idhan_server_version" ][ "minor" ] = IDHAN_MINOR_VERSION;
json[ "idhan_server_version" ][ "patch" ] = IDHAN_PATCH_VERSION;
json[ "idhan_api_version" ][ "string" ] =
format_ns::format( "{}.{}.{}", IDHAN_API_MAJOR, IDHAN_API_MINOR, IDHAN_API_PATCH );
@@ -37,6 +39,7 @@ drogon::Task< drogon::HttpResponsePtr > InfoAPI::version( [[maybe_unused]] drogo
json[ "commit" ] = FGL_GIT_COMMIT;
json[ "tag" ] = FGL_GIT_TAG;
json[ "build" ] = FGL_BUILD_TYPE;
json[ "build_on" ] = IDHAN_BUILD_DATE ", " IDHAN_BUILD_TIME;
co_return drogon::HttpResponse::newHttpJsonResponse( json );
}

View File

@@ -0,0 +1,12 @@
//
// Created by kj16609 on 11/8/25.
//
#pragma once
#include "idhan/versions.hpp"
#define IDHAN_API_MAJOR 0
#define IDHAN_API_MINOR 0
#define IDHAN_API_PATCH 0
#define IDHAN_API_VERSION MAKE_IDHAN_VERSION( IDHAN_API_MAJOR, IDHAN_API_MINOR, IDHAN_API_PATCH )

View File

@@ -8,6 +8,7 @@
#include "api/TagAPI.hpp"
#include "api/helpers/createBadRequest.hpp"
#include "api/helpers/records.hpp"
#include "api/version.hpp"
#include "constants/hydrus_version.hpp"
#include "core/search/SearchBuilder.hpp"
#include "crypto/SHA256.hpp"
@@ -19,7 +20,6 @@
#include "logging/ScopedTimer.hpp"
#include "logging/log.hpp"
#include "metadata/parseMetadata.hpp"
#include "versions.hpp"
namespace idhan::hyapi
{

View File

@@ -149,6 +149,12 @@ int main( int argc, char** argv )
}
}
log::info(
"Starting IDHAN context v{}.{}.{}",
IDHAN_MAJOR_VERSION,
IDHAN_MINOR_VERSION,
IDHAN_PATCH_VERSION );
idhan::ServerContext context { arguments };
context.run();

View File

@@ -1,71 +0,0 @@
//
// Created by kj16609 on 7/30/24.
//
#pragma once
#define MAKE_IDHAN_VERSION( major, minor, patch ) int( ( major << 16 ) | ( minor < 8 ) || patch )
#define IDHAN_API_MAJOR 0
#define IDHAN_API_MINOR 0
#define IDHAN_API_PATCH 0
#define IDHAN_SERVER_MAJOR 0
#define IDHAN_SERVER_MINOR 0
#define IDHAN_SERVER_PATCH 0
#ifdef NDEBUG
#ifndef IDHAN_SERVER_MAJOR
#error Major version must be specified for release builds
#endif
#ifndef IDHAN_SERVER_MINOR
#error Minor version must be specified for release builds
#endif
#ifndef IDHAN_SERVER_PATCH
#error Patch version must be specified for release builds
#endif
#ifndef IDHAN_API_MAJOR
#error Major version must be specified for release builds
#endif
#ifndef IDHAN_API_MINOR
#error Minor version must be specified for release builds
#endif
#ifndef IDHAN_API_PATCH
#error Patch version must be specified for release builds
#endif
#else
#ifndef IDHAN_SERVER_MAJOR
#define IDHAN_SERVER_MAJOR 0
#endif
#ifndef IDHAN_SERVER_MINOR
#define IDHAN_SERVER_MINOR 0
#endif
#ifndef IDHAN_SERVER_PATCH
#define IDHAN_SERVER_PATCH 0
#endif
#ifndef IDHAN_API_MAJOR
#define IDHAN_API_MAJOR 0
#endif
#ifndef IDHAN_API_MINOR
#define IDHAN_API_MINOR 0
#endif
#ifndef IDHAN_API_PATCH
#define IDHAN_API_PATCH 0
#endif
#endif
#define IDHAN_SERVER_VERSION MAKE_IDHAN_VERSION( IDHAN_SERVER_MAJOR, IDHAN_SERVER_MINOR, IDHAN_SERVER_PATCH )
#define IDHAN_API_VERSION MAKE_IDHAN_VERSION( IDHAN_API_MAJOR, IDHAN_API_MINOR, IDHAN_API_PATCH )