metube/app/mark_watched.py

229 lines
7.7 KiB
Python

"""
Mark videos as watched on various websites after successful download.
Uses the same cookies that were used for downloading.
"""
import os
import re
import logging
from urllib.parse import urlparse, parse_qs
from typing import Optional, Dict, Callable
from headless_watcher import headless_mark_watched, HAS_PLAYWRIGHT
log = logging.getLogger("mark_watched")
# Try to import curl_cffi for better impersonation, fallback to requests
try:
from curl_cffi import requests as curl_requests
HAS_CURL_CFFI = True
except ImportError:
HAS_CURL_CFFI = False
import requests
class MarkWatchedHandler:
"""Base class for mark-as-watched handlers."""
def __init__(self, cookie_file: str):
self.cookie_file = cookie_file
def can_handle(self, url: str) -> bool:
"""Check if this handler can handle the given URL."""
raise NotImplementedError
async def mark_watched(self, url: str) -> bool:
"""Mark the video as watched. Returns True on success."""
raise NotImplementedError
def _load_cookies(self) -> Dict[str, str]:
"""Load cookies from the Netscape cookie file."""
cookies = {}
if not os.path.exists(self.cookie_file):
log.warning(f"Cookie file not found: {self.cookie_file}")
return cookies
try:
with open(self.cookie_file, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
# Netscape format: domain flag path secure expiration name value
parts = line.split("\t")
if len(parts) >= 7:
name = parts[5]
value = parts[6]
cookies[name] = value
log.debug(f"Loaded {len(cookies)} cookies from {self.cookie_file}")
except Exception as e:
log.error(f"Error loading cookies: {e}")
return cookies
def _make_request(self, method: str, url: str, **kwargs) -> Optional[object]:
"""Make HTTP request using curl_cffi if available, else requests."""
try:
if HAS_CURL_CFFI:
# Use curl_cffi for better impersonation
session = curl_requests.Session()
# Set browser impersonation
session.impersonate = "chrome124"
response = session.request(method, url, **kwargs)
return response
else:
return requests.request(method, url, **kwargs)
except Exception as e:
log.error(f"Request failed: {e}")
return None
class PHHandler(MarkWatchedHandler):
"""Handler for PornHub."""
DOMAINS = ["pornhub.com", "www.pornhub.com", "de.pornhub.com", "fr.pornhub.com", "es.pornhub.com", "it.pornhub.com", "rt.pornhub.com"]
def can_handle(self, url):
parsed = urlparse(url)
domain = parsed.netloc.lower()
return any(d in domain for d in self.DOMAINS)
async def mark_watched(self, url):
# Extract viewkey from URL
parsed = urlparse(url)
params = parse_qs(parsed.query)
viewkey = params.get("viewkey", [None])[0]
if not viewkey:
# Try to extract from path
match = re.search(r"viewkey=([^&]+)", url)
if match:
viewkey = match.group(1)
if not viewkey:
log.warning(f"Could not extract viewkey from URL: {url}")
return False
log.info(f"Marking video as watched, viewkey: {viewkey}")
# Load cookies
cookies = self._load_cookies()
if not cookies:
log.warning("No cookies available, cannot mark as watched")
return False
# Try multiple endpoints as PH may use different URLs
# First try the AJAX endpoint which is most commonly used
endpoints = [
# AJAX endpoint (most common)
("POST", f"https://www.pornhub.com/user/watched/add/video/{viewkey}"),
# Alternative format
("GET", f"https://www.pornhub.com/user/watched/add/video/{viewkey}"),
# Legacy endpoint
("POST", f"https://www.pornhub.com/user/watched/video/viewkey/{viewkey}"),
]
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "en-US,en;q=0.9",
"X-Requested-With": "XMLHttpRequest",
"Referer": url,
}
for method, api_url in endpoints:
log.debug(f"Trying {method} {api_url}")
response = self._make_request(method, api_url, headers=headers, cookies=cookies, allow_redirects=True)
if response is None:
continue
log.debug(f"Response status: {response.status_code}")
if response.status_code == 200:
log.info(f"Successfully marked video as watched using {api_url}")
return True
elif response.status_code == 302:
# Redirect often means success on PH
log.info(f"Successfully marked video as watched (redirect from {api_url})")
return True
# API methods failed, try headless browser as fallback
log.info("API methods failed, trying headless browser approach")
if HAS_PLAYWRIGHT:
return await headless_mark_watched(url, self.cookie_file, wait_seconds=5)
else:
log.warning("Playwright not available for headless browser fallback")
return False
class YouTubeHandler(MarkWatchedHandler):
"""Handler for YouTube - uses YouTube API or internal endpoints."""
DOMAINS = ["youtube.com", "www.youtube.com", "youtu.be", "m.youtube.com"]
def can_handle(self, url):
parsed = urlparse(url)
domain = parsed.netloc.lower()
return any(d in domain for d in self.DOMAINS)
async def mark_watched(self, url):
# YouTube marking as watched requires more complex handling
# typically done through the browse endpoint with protobuf
# This is a simplified implementation
log.info("YouTube mark-as-watched not yet fully implemented")
return False
# Registry of all handlers
HANDLERS = [
PHHandler,
YouTubeHandler,
]
def get_handler(url: str, cookie_file: str) -> Optional[MarkWatchedHandler]:
"""
Get the appropriate handler for a URL.
Args:
url: The video URL
cookie_file: Path to the Netscape cookie file
Returns:
Handler instance if found, None otherwise
"""
if not cookie_file or not os.path.exists(cookie_file):
return None
for handler_class in HANDLERS:
handler = handler_class(cookie_file)
if handler.can_handle(url):
return handler
return None
async def mark_watched(url: str, cookie_file: str) -> bool:
"""
Mark a video as watched on its respective site.
Args:
url: The video URL
cookie_file: Path to the Netscape cookie file
Returns:
True if successfully marked as watched, False otherwise
"""
handler = get_handler(url, cookie_file)
if not handler:
log.debug(f"No mark-watched handler available for URL: {url}")
return False
try:
return await handler.mark_watched(url)
except Exception as e:
log.error(f"Error marking video as watched: {e}")
return False