adding current revision information
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user