adding current revision information

This commit is contained in:
2026-01-02 08:29:16 -05:00
parent 789465483c
commit 920947e556
64 changed files with 5963 additions and 18 deletions
@@ -263,4 +263,58 @@ class DuplicatesManager:
return {"report_id": report.id, "status": "completed"}
def get_reports(self):
return self.db.query(models.DuplicateReport).all()
def get_duplicate_report(self, report_id: int):
report = self.db.query(models.DuplicateReport).filter(models.DuplicateReport.id == report_id).first()
if not report:
return {"error": "Report not found"}
groups = []
for group in report.groups:
files = []
for duplicate_file in group.files:
files.append(duplicate_file.video_file)
groups.append({
"group_id": group.id,
"score": group.score,
"files": files,
})
return {
"report_id": report.id,
"created_at": report.created_at,
"status": report.status,
"groups": groups,
}
def delete_files(self, filepaths: list[str]):
samba_manager = SambaManager(
self.settings.samba_server_ip,
"isolation", # Assuming all duplicates are in the isolation share
self.settings.samba_username,
self.settings.samba_password,
)
try:
for filepath in filepaths:
# Delete from Samba
samba_manager.delete_file(filepath)
# Delete from database
video_file = self.db.query(models.VideoFile).filter_by(filepath=filepath).first()
if video_file:
# Delete associations in DuplicateFile
self.db.query(models.DuplicateFile).filter_by(video_file_id=video_file.id).delete()
self.db.delete(video_file)
self.db.commit()
return {"status": "success"}
except Exception as e:
self.db.rollback()
logger.error(f"Error deleting files: {e}", exc_info=True)
return {"status": "error", "message": str(e)}
finally:
samba_manager.close()
@@ -1,13 +1,18 @@
from fastapi import APIRouter, Depends, BackgroundTasks
from sqlalchemy.orm import Session
from pydantic import BaseModel
from ..duplicates_manager import DuplicatesManager
from ..config import settings, Settings
from ..database import get_db, SessionLocal
from ..samba_manager import SambaManager
import os
from typing import List
router = APIRouter()
class DeleteFilesRequest(BaseModel):
filepaths: List[str]
def run_scan_videos_background():
db = SessionLocal()
app_settings = Settings()
@@ -55,3 +60,18 @@ def find_duplicates(threshold: float = 0.95, db: Session = Depends(get_db)):
manager = DuplicatesManager(settings, db)
result = manager.find_duplicates(threshold)
return result
@router.get("/reports")
def get_reports(db: Session = Depends(get_db)):
manager = DuplicatesManager(settings, db)
return manager.get_reports()
@router.get("/report/{report_id}")
def get_report(report_id: int, db: Session = Depends(get_db)):
manager = DuplicatesManager(settings, db)
return manager.get_duplicate_report(report_id)
@router.delete("/files")
def delete_files(request: DeleteFilesRequest, db: Session = Depends(get_db)):
manager = DuplicatesManager(settings, db)
return manager.delete_files(request.filepaths)
@@ -1 +0,0 @@
/videos/castingcurvy/Videos
@@ -5,29 +5,95 @@
<button @click="pauseScan" :disabled="isLoading">Pause Scan</button>
<button @click="resumeScan" :disabled="isLoading">Resume Scan</button>
<p>Status: {{ status }}</p>
<hr>
<h2>Find Duplicates</h2>
<div>
<label for="threshold-slider">Similarity Threshold: {{ threshold }}</label>
<input
type="range"
id="threshold-slider"
min="0.8"
max="1.0"
step="0.01"
v-model="threshold"
/>
<button @click="findDuplicates" :disabled="isLoading">Find Duplicates</button>
</div>
<div v-if="duplicatesReport">
<h3>Duplicate Report #{{ duplicatesReport.report_id }} ({{ duplicatesReport.status }})</h3>
<button @click="deleteSelectedFiles" :disabled="selectedFiles.length === 0">Delete Selected</button>
<div v-for="group in duplicatesReport.groups" :key="group.group_id" class="duplicate-group">
<h4>Group {{ group.group_id }} (Score: {{ group.score.toFixed(4) }})</h4>
<ul>
<li v-for="file in group.files" :key="file.id">
<input type="checkbox" :value="file.filepath" v-model="selectedFiles" />
{{ file.filepath }} ({{ (file.size / 1024 / 1024).toFixed(2) }} MB)
<button @click="viewFile(file.filepath)">View</button>
</li>
</ul>
</div>
</div>
<hr>
<h2>Previous Reports</h2>
<ul>
<li v-for="report in reports" :key="report.id">
<a href="#" @click.prevent="getReport(report.id)">
Report #{{ report.id }} - {{ new Date(report.created_at).toLocaleString() }} ({{ report.status }})
</a>
</li>
</ul>
<FileViewer
v-if="fileToView"
:file-path="fileToView"
:share-name="shareName"
@close="closeViewer"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import FileViewer from './FileViewer.vue';
const status = ref('Idle');
const isLoading = ref(false);
const threshold = ref(0.95);
const duplicatesReport = ref(null);
const reports = ref([]);
const selectedFiles = ref([]);
const fileToView = ref(null);
const shareName = computed(() => {
if (fileToView.value) {
// This is a simplification. In a real app, you might have a more robust way
// to determine the share from the path.
if (fileToView.value.startsWith('/videos')) {
return 'isolation';
}
}
return 'media'; // Default share
});
const getStatus = async () => {
// This endpoint doesn't exist yet, but I'm adding the function in preparation.
// try {
// const response = await fetch('/api/duplicates/scan/status');
// if (response.ok) {
// const data = await response.json();
// status.value = data.status;
// } else {
// status.value = 'Error fetching status';
// }
// } catch (error) {
// status.value = 'Error fetching status';
// console.error('Error fetching status:', error);
// }
try {
const response = await fetch('/api/duplicates/scan/status');
if (response.ok) {
const data = await response.json();
status.value = data.status;
} else {
status.value = 'Error fetching status';
}
} catch (error) {
status.value = 'Error fetching status';
console.error('Error fetching status:', error);
}
};
const startScan = async () => {
@@ -71,7 +137,8 @@ const resumeScan = async () => {
const response = await fetch('/api/duplicates/scan/resume', { method: 'POST' });
if (response.ok) {
status.value = 'Scan resumed.';
} else {
}
else {
status.value = 'Error resuming scan.';
}
} catch (error) {
@@ -81,9 +148,94 @@ const resumeScan = async () => {
isLoading.value = false;
};
const getReports = async () => {
try {
const response = await fetch('/api/duplicates/reports');
if (response.ok) {
reports.value = await response.json();
} else {
console.error('Error fetching reports');
}
} catch (error) {
console.error('Error fetching reports:', error);
}
};
const getReport = async (reportId) => {
try {
const response = await fetch(`/api/duplicates/report/${reportId}`);
if (response.ok) {
duplicatesReport.value = await response.json();
selectedFiles.value = []; // Clear selection when a new report is loaded
} else {
console.error('Error fetching report');
}
} catch (error) {
console.error('Error fetching report:', error);
}
};
const findDuplicates = async () => {
isLoading.value = true;
duplicatesReport.value = null;
try {
const response = await fetch(`/api/duplicates/find?threshold=${threshold.value}`, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
if (data.report_id) {
await getReport(data.report_id);
}
await getReports(); // Refresh the list of reports
} else {
console.error('Error finding duplicates');
}
} catch (error) {
console.error('Error finding duplicates:', error);
}
isLoading.value = false;
};
const viewFile = (filePath) => {
fileToView.value = filePath;
};
const closeViewer = () => {
fileToView.value = null;
};
const deleteSelectedFiles = async () => {
if (!confirm(`Are you sure you want to delete ${selectedFiles.value.length} files?`)) {
return;
}
isLoading.value = true;
try {
const response = await fetch('/api/duplicates/files', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ filepaths: selectedFiles.value }),
});
if (response.ok) {
// Refresh the report
if (duplicatesReport.value) {
await getReport(duplicatesReport.value.report_id);
}
} else {
console.error('Error deleting files');
}
} catch (error) {
console.error('Error deleting files:', error);
}
isLoading.value = false;
};
onMounted(() => {
// getStatus();
// setInterval(getStatus, 5000); // Poll for status every 5 seconds
getStatus();
getReports();
setInterval(getStatus, 5000); // Poll for status every 5 seconds
});
</script>
@@ -91,4 +243,12 @@ onMounted(() => {
button {
margin: 5px;
}
hr {
margin: 20px 0;
}
.duplicate-group {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
</style>