mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
336 lines
12 KiB
Python
336 lines
12 KiB
Python
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView
|
|
from django.urls import reverse_lazy
|
|
from django.http import JsonResponse, HttpResponseRedirect
|
|
from django.shortcuts import get_object_or_404
|
|
from django.contrib import messages
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.core.files.base import ContentFile
|
|
from django.conf import settings
|
|
from rest_framework import viewsets, status
|
|
from rest_framework.response import Response
|
|
from rest_framework.pagination import PageNumberPagination
|
|
from .models import Page, Screenshot, Tag
|
|
from .forms import PageForm
|
|
from .serializers import PageSerializer
|
|
import os
|
|
import requests
|
|
from bs4 import BeautifulSoup
|
|
from urllib.parse import urlparse, quote
|
|
import re
|
|
import base64
|
|
from .tasks import capture_screenshot, process_page, fetch_webpage_content_from_crawl4ai
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from rest_framework.decorators import action
|
|
from threading import Thread
|
|
|
|
|
|
def trigger_page_processing(page):
|
|
"""
|
|
Trigger background tasks for a newly created page:
|
|
1. Screenshot capture
|
|
2. Crawl4AI content extraction
|
|
3. Page metadata processing
|
|
|
|
This is called whenever a page is created, regardless of the source (UI or API).
|
|
"""
|
|
from .tasks import capture_screenshot, fetch_webpage_content_from_crawl4ai, process_page
|
|
|
|
# Create screenshot record and trigger capture
|
|
screenshot = Screenshot.objects.create(
|
|
page=page,
|
|
status=Screenshot.Status.PENDING
|
|
)
|
|
|
|
# Start screenshot capture thread
|
|
thread_screenshot = Thread(target=capture_screenshot, args=(page.id, screenshot.id))
|
|
thread_screenshot.daemon = True
|
|
thread_screenshot.start()
|
|
|
|
# Start Crawl4AI content extraction thread
|
|
thread_crawl = Thread(target=fetch_webpage_content_from_crawl4ai, args=(page.id,))
|
|
thread_crawl.daemon = True
|
|
thread_crawl.start()
|
|
|
|
# Start page metadata processing thread (if needed)
|
|
if not page.title or not page.summary:
|
|
thread_process = Thread(target=process_page, args=(page.id,))
|
|
thread_process.daemon = True
|
|
thread_process.start()
|
|
|
|
|
|
class PageListView(ListView):
|
|
model = Page
|
|
template_name = 'links/page_list.html'
|
|
context_object_name = 'pages'
|
|
paginate_by = 10
|
|
|
|
class PageDetailView(DetailView):
|
|
model = Page
|
|
template_name = 'links/page_detail.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['all_tags'] = Tag.objects.all()
|
|
return context
|
|
|
|
class PageCreateView(CreateView):
|
|
model = Page
|
|
form_class = PageForm
|
|
template_name = 'links/page_form.html'
|
|
success_url = reverse_lazy('page-list')
|
|
|
|
def form_valid(self, form):
|
|
response = super().form_valid(form)
|
|
page = self.object
|
|
|
|
# Trigger all background processing tasks
|
|
try:
|
|
trigger_page_processing(page)
|
|
messages.success(self.request, _('Page created successfully and processing started.'))
|
|
except Exception as e:
|
|
messages.error(self.request, _('Page created but processing failed to start.'))
|
|
page.error_message = str(e)
|
|
page.save()
|
|
|
|
return response
|
|
|
|
class PageUpdateView(UpdateView):
|
|
model = Page
|
|
form_class = PageForm
|
|
template_name = 'links/page_form.html'
|
|
success_url = reverse_lazy('page-list')
|
|
|
|
def form_valid(self, form):
|
|
response = super().form_valid(form)
|
|
|
|
# Check if reset_status was checked
|
|
if self.request.POST.get('reset_status'):
|
|
self.object.process_status = Page.ProcessStatus.PENDING
|
|
self.object.retry_count = 0
|
|
self.object.error_message = ''
|
|
self.object.screenshot_path = ''
|
|
self.object.save()
|
|
|
|
# Trigger processing task
|
|
from .tasks import process_page
|
|
thread = Thread(target=process_page, args=(self.object.id,))
|
|
thread.daemon = True
|
|
thread.start()
|
|
|
|
return response
|
|
|
|
class PageDeleteView(DeleteView):
|
|
model = Page
|
|
template_name = 'links/page_confirm_delete.html'
|
|
success_url = reverse_lazy('page-list')
|
|
|
|
def fetch_page_info(request):
|
|
url = request.GET.get('url')
|
|
if not url:
|
|
return JsonResponse({'error': 'URL is required'}, status=400)
|
|
|
|
url = url.lstrip('@')
|
|
|
|
try:
|
|
headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
'Accept-Language': 'en-US,en;q=0.5',
|
|
}
|
|
|
|
response = requests.get(url, headers=headers, timeout=5, verify=False) # Reduced timeout to 5 seconds
|
|
if response.encoding == 'ISO-8859-1':
|
|
response.encoding = response.apparent_encoding or 'utf-8'
|
|
|
|
soup = BeautifulSoup(response.text, 'html.parser')
|
|
|
|
# Get title
|
|
title = None
|
|
if soup.title:
|
|
title = soup.title.string
|
|
elif soup.find('h1'):
|
|
title = soup.find('h1').get_text(strip=True)
|
|
elif soup.find('meta', property='og:title'):
|
|
title = soup.find('meta', property='og:title').get('content')
|
|
|
|
if title:
|
|
title = re.sub(r'\s+', ' ', title.strip())
|
|
title = title.replace(' | ', ' - ').replace(' :: ', ' - ')
|
|
else:
|
|
title = urlparse(url).netloc
|
|
|
|
# Get description
|
|
description = None
|
|
meta_desc = soup.find('meta', {'name': 'description'}) or soup.find('meta', {'property': 'og:description'})
|
|
if meta_desc:
|
|
description = meta_desc.get('content')
|
|
|
|
if not description:
|
|
for tag in soup(['script', 'style', 'nav', 'header', 'footer']):
|
|
tag.decompose()
|
|
|
|
paragraphs = soup.find_all(['p', 'div'])
|
|
for p in paragraphs:
|
|
text = p.get_text(strip=True)
|
|
if len(text) > 100:
|
|
description = text
|
|
break
|
|
|
|
if not description:
|
|
description = title
|
|
|
|
description = re.sub(r'\s+', ' ', description.strip())
|
|
description = description[:500] + '...' if len(description) > 500 else description
|
|
|
|
return JsonResponse({
|
|
'title': title,
|
|
'summary': description
|
|
})
|
|
|
|
except Exception as e:
|
|
return JsonResponse({
|
|
'error': f'Error processing page: {str(e)}'
|
|
}, status=400)
|
|
|
|
class StandardResultsSetPagination(PageNumberPagination):
|
|
page_size = 10
|
|
page_size_query_param = 'page_size'
|
|
max_page_size = 100
|
|
|
|
class PageViewSet(viewsets.ModelViewSet):
|
|
queryset = Page.objects.all().order_by('-created_at')
|
|
serializer_class = PageSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_queryset(self):
|
|
return Page.objects.all().order_by('-created_at')
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
self.perform_create(serializer)
|
|
headers = self.get_success_headers(serializer.data)
|
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
def perform_create(self, serializer):
|
|
instance = serializer.save()
|
|
if instance.title and instance.summary:
|
|
instance.process_status = Page.ProcessStatus.COMPLETED
|
|
else:
|
|
instance.process_status = Page.ProcessStatus.PENDING
|
|
instance.save()
|
|
|
|
# Trigger all background processing tasks
|
|
trigger_page_processing(instance)
|
|
|
|
@action(detail=True, methods=['post'], url_path='take-screenshot')
|
|
def take_screenshot(self, request, pk=None):
|
|
"""API endpoint to take a new screenshot of a page"""
|
|
page = self.get_object()
|
|
|
|
# Create new Screenshot instance
|
|
screenshot = Screenshot.objects.create(
|
|
page=page,
|
|
status=Screenshot.Status.PENDING
|
|
)
|
|
|
|
# Trigger screenshot capture task
|
|
try:
|
|
thread = Thread(target=capture_screenshot, args=(page.id, screenshot.id))
|
|
thread.daemon = True
|
|
thread.start()
|
|
return Response({
|
|
'status': 'success',
|
|
'message': 'Screenshot capture initiated',
|
|
'screenshot_id': screenshot.id
|
|
})
|
|
except Exception as e:
|
|
screenshot.status = Screenshot.Status.FAILED
|
|
screenshot.error = str(e)
|
|
screenshot.save()
|
|
return Response({
|
|
'status': 'error',
|
|
'error': str(e)
|
|
}, status=400)
|
|
|
|
@action(detail=False, methods=['delete'], url_path='screenshots/(?P<screenshot_id>[^/.]+)')
|
|
def delete_screenshot(self, request, screenshot_id=None):
|
|
"""Delete a specific screenshot"""
|
|
try:
|
|
screenshot = Screenshot.objects.get(id=screenshot_id)
|
|
|
|
# Optional: Add permission check here
|
|
# if screenshot.page.user != request.user:
|
|
# return Response(status=status.HTTP_403_FORBIDDEN)
|
|
|
|
# Delete the actual file
|
|
if screenshot.path:
|
|
file_path = os.path.join(settings.MEDIA_ROOT, screenshot.path)
|
|
try:
|
|
os.remove(file_path)
|
|
except OSError:
|
|
pass # File doesn't exist or can't be deleted
|
|
|
|
# Delete the database record
|
|
screenshot.delete()
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
except Screenshot.DoesNotExist:
|
|
return Response(
|
|
{"error": "Screenshot not found"},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
except Exception as e:
|
|
return Response(
|
|
{"error": str(e)},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
class ScreenshotGalleryView(TemplateView):
|
|
template_name = 'links/screenshot_gallery.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
screenshots_dir = os.path.join(settings.MEDIA_ROOT, 'screenshots')
|
|
screenshots = []
|
|
|
|
if os.path.exists(screenshots_dir):
|
|
for filename in os.listdir(screenshots_dir):
|
|
if filename.endswith('.png'):
|
|
file_path = os.path.join(screenshots_dir, filename)
|
|
# Extract page ID and clean up filename
|
|
name_parts = filename.split('_', 2) # Split into ['page', id, 'rest_of_name']
|
|
if len(name_parts) >= 2:
|
|
id = name_parts[1].replace('.png', '') # Remove .png extension
|
|
else:
|
|
id = '1'
|
|
if len(name_parts) >= 3:
|
|
display_name = name_parts[2].replace('.png', '')
|
|
else:
|
|
display_name = filename.replace('.png', '')
|
|
|
|
screenshots.append({
|
|
'url': f'/media/screenshots/{filename}',
|
|
'created': datetime.fromtimestamp(os.path.getctime(file_path)),
|
|
'display_name': display_name,
|
|
'id': id
|
|
})
|
|
|
|
# Sort screenshots by creation time, newest first
|
|
screenshots.sort(key=lambda x: x['created'], reverse=True)
|
|
context['screenshots'] = screenshots
|
|
return context
|
|
|
|
def update_page_tags(request, pk):
|
|
if request.method == 'POST':
|
|
page = get_object_or_404(Page, pk=pk)
|
|
selected_tags = request.POST.getlist('tags')
|
|
page.tags.clear()
|
|
if selected_tags:
|
|
page.tags.add(*selected_tags)
|
|
messages.success(request, _('Tags updated successfully'))
|
|
return HttpResponseRedirect(reverse('page-detail', args=[pk]))
|
|
return HttpResponseRedirect(reverse('page-detail', args=[pk]))
|