Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ebff6ee4d | |||
| b00e94c6a0 | |||
| 3256304107 | |||
| c045f326b5 | |||
| 57923c4cab | |||
| 2ba6d73091 | |||
| eec15b9fb2 | |||
| c5f556d48e | |||
| 4871de36a1 | |||
| 9aa071b3c7 |
@@ -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 ()
|
||||
|
||||
@@ -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 )
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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><html><head/><body><p>Only processes mappings for files that you've obtained</p></body></html></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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 & 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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 };
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
#include <deque>
|
||||
|
||||
#include "HydrusImporter.hpp"
|
||||
#include "HydrusImporterWidget.hpp"
|
||||
|
||||
class TagServiceWorker;
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
213
HydrusImporter/src/gui/hydrus/tag_service/TagServiceWidget.ui
Normal file
213
HydrusImporter/src/gui/hydrus/tag_service/TagServiceWidget.ui
Normal 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><html><head/><body><p>Only processes mappings for files that you've obtained</p></body></html></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>
|
||||
@@ -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"
|
||||
58
HydrusImporter/src/gui/hydrus/urls/UrlServiceWidget.cpp
Normal file
58
HydrusImporter/src/gui/hydrus/urls/UrlServiceWidget.cpp
Normal 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 ) );
|
||||
}
|
||||
47
HydrusImporter/src/gui/hydrus/urls/UrlServiceWidget.hpp
Normal file
47
HydrusImporter/src/gui/hydrus/urls/UrlServiceWidget.hpp
Normal 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
|
||||
142
HydrusImporter/src/gui/hydrus/urls/UrlServiceWidget.ui
Normal file
142
HydrusImporter/src/gui/hydrus/urls/UrlServiceWidget.ui
Normal 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>
|
||||
105
HydrusImporter/src/gui/hydrus/urls/UrlServiceWorker.cpp
Normal file
105
HydrusImporter/src/gui/hydrus/urls/UrlServiceWorker.cpp
Normal 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();
|
||||
}
|
||||
29
HydrusImporter/src/gui/hydrus/urls/UrlServiceWorker.hpp
Normal file
29
HydrusImporter/src/gui/hydrus/urls/UrlServiceWorker.hpp
Normal 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;
|
||||
};
|
||||
@@ -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 )
|
||||
{
|
||||
|
||||
@@ -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 )
|
||||
{
|
||||
|
||||
@@ -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})
|
||||
|
||||
24
IDHAN/include/idhan/versions.hpp
Normal file
24
IDHAN/include/idhan/versions.hpp
Normal 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__
|
||||
@@ -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 );
|
||||
|
||||
@@ -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(
|
||||
|
||||
54
IDHANClient/src/records/addUrls.cpp
Normal file
54
IDHANClient/src/records/addUrls.cpp
Normal 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
|
||||
46
IDHANClient/src/records/relationships/setAlternatives.cpp
Normal file
46
IDHANClient/src/records/relationships/setAlternatives.cpp
Normal 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
|
||||
57
IDHANClient/src/records/relationships/setDuplicates.cpp
Normal file
57
IDHANClient/src/records/relationships/setDuplicates.cpp
Normal 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
|
||||
53
IDHANMigration/src/130-alternative_files.sql
Normal file
53
IDHANMigration/src/130-alternative_files.sql
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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 )
|
||||
|
||||
26
IDHANServer/src/api/FileRelationshipsAPI.hpp
Normal file
26
IDHANServer/src/api/FileRelationshipsAPI.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
124
IDHANServer/src/api/relationships/addAlternative.cpp
Normal file
124
IDHANServer/src/api/relationships/addAlternative.cpp
Normal 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
|
||||
61
IDHANServer/src/api/relationships/setBetterDuplicate.cpp
Normal file
61
IDHANServer/src/api/relationships/setBetterDuplicate.cpp
Normal 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
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
12
IDHANServer/src/api/version.hpp
Normal file
12
IDHANServer/src/api/version.hpp
Normal 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 )
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 )
|
||||
2
dependencies/libFGL
vendored
2
dependencies/libFGL
vendored
Submodule dependencies/libFGL updated: 7bab49f8d8...d1d58e4eba
Reference in New Issue
Block a user