From 0b955ed2b945eedc66ac39b4eb31083bc8fd365a Mon Sep 17 00:00:00 2001 From: zphinx Date: Sun, 22 Feb 2026 03:20:33 +0100 Subject: [PATCH] push --- svt.sh | 434 +++++++-------------------------------------------------- 1 file changed, 52 insertions(+), 382 deletions(-) diff --git a/svt.sh b/svt.sh index 7187415..43ec50a 100755 --- a/svt.sh +++ b/svt.sh @@ -1,19 +1,13 @@ #!/bin/bash - -# SVT Play Stream Launcher - 100% GUI Based -# Search and stream SVT Play content with zenity interface +# SVT Play Stream Launcher - Search and stream SVT Play content with zenity set -euo pipefail -# Debug mode DEBUG="${DEBUG:-0}" WAIT_FOR_MPV=0 - -# Logging LOG_FILE="${HOME}/.svtplay.log" -mkdir -p "$(dirname "$LOG_FILE")" -# Parse command line options +# Parse options while [[ $# -gt 0 ]]; do case "$1" in --debug) DEBUG=1; shift ;; @@ -21,438 +15,114 @@ while [[ $# -gt 0 ]]; do --log) tail -f "$LOG_FILE"; exit 0 ;; --clear-log) rm -f "$LOG_FILE"; echo "Log cleared"; exit 0 ;; --help|-h) - zenity --info --title="SVT Play Launcher" --text="SVT Play Stream Launcher\n\nA GUI-based tool to search and stream SVT Play content.\n\nUsage: ./svt.sh [OPTIONS]\n\nOptions:\n --debug Enable debug output\n --wait Wait for playback to finish\n --log View live log\n --help Show this message" - exit 0 - ;; + zenity --info --title="SVT Play" --text="Usage: ./svt.sh [--debug|--wait|--log|--help]" + exit 0 ;; *) break ;; esac done -# Color definitions -readonly RED='\033[0;31m' -readonly GREEN='\033[0;32m' -readonly YELLOW='\033[1;33m' -readonly BLUE='\033[0;34m' -readonly NC='\033[0m' +# Logging +log() { echo -e "\033[0;34m[$1]\033[0m $2" >&2; echo "[$(date '+%H:%M:%S')] [$1] $2" >> "$LOG_FILE"; } +log_err() { echo -e "\033[0;31m[ERROR]\033[0m $1" >&2; echo "[$(date '+%H:%M:%S')] [ERROR] $1" >> "$LOG_FILE"; } -# Zenity dark mode helper -zenity_dark() { - GTK_THEME=Adwaita:dark zenity "$@" -} +# Force dark mode - works with GTK3 (GTK_THEME) and GTK4/libadwaita (ADW_DEBUG_COLOR_SCHEME) +zenity_dark() { GTK_THEME=Adwaita:dark ADW_DEBUG_COLOR_SCHEME=prefer-dark zenity "$@"; } -# Logging functions -log_info() { - local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*" - echo -e "${BLUE}[INFO]${NC} $*" >&2 - echo "$msg" >> "$LOG_FILE" -} - -log_success() { - local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [✓] $*" - echo -e "${GREEN}[✓]${NC} $*" >&2 - echo "$msg" >> "$LOG_FILE" -} - -log_error() { - local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" - echo -e "${RED}[ERROR]${NC} $*" >&2 - echo "$msg" >> "$LOG_FILE" -} - -log_debug() { - if [[ "$DEBUG" == "1" ]]; then - local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [DEBUG] $*" - echo -e "${BLUE}[DEBUG]${NC} $*" >&2 - echo "$msg" >> "$LOG_FILE" - fi -} - -# Check dependencies check_dependencies() { - log_info "Checking dependencies..." - - local required=("yt-dlp" "mpv" "zenity" "jq" "curl") local missing=() - - for cmd in "${required[@]}"; do - if ! command -v "$cmd" &>/dev/null; then - missing+=("$cmd") - fi + for cmd in yt-dlp mpv zenity jq curl; do + command -v "$cmd" &>/dev/null || missing+=("$cmd") done - if [[ ${#missing[@]} -gt 0 ]]; then - log_error "Missing required tools: ${missing[*]}" - zenity_dark --error --title="Missing Dependencies" --text="Please install: ${missing[*]}\n\nArch: sudo pacman -S ${missing[*]}\nDebian: sudo apt install ${missing[*]}" + log_err "Missing: ${missing[*]}" + zenity_dark --error --title="Missing Dependencies" --text="Please install: ${missing[*]}" exit 1 fi - - log_success "Dependencies OK" } -# Search programs via SVT Play GraphQL API search_programs() { local query="$1" - log_info "API: Querying searchPage with: $query" + log "API" "Searching: $query" - # Make API call with searchPage (with longer timeout since SVT API can be slow) - local response - response=$(curl -s --connect-timeout 10 --max-time 30 -w "\n%{http_code}" -X POST "https://api.svt.se/contento/graphql" \ - -H "Content-Type: application/json" \ - -d "{\"query\": \"{ searchPage(query: \\\"${query}\\\", maxHits: 50) { flat { hits { teaser { heading } } } } }\"}" 2>/dev/null) || { - log_error "Failed to query SVT Play API (curl error)" - return 1 - } + local response http_code + response=$(curl -s --connect-timeout 10 --max-time 30 -w "\n%{http_code}" -X POST \ + "https://api.svt.se/contento/graphql" -H "Content-Type: application/json" \ + -d "{\"query\": \"{ searchPage(query: \\\"${query}\\\", maxHits: 50) { flat { hits { teaser { heading item { urls { svtplay } } } } } } }\"}" 2>/dev/null) || return 1 - local http_code=$(echo "$response" | tail -1) + http_code=$(echo "$response" | tail -1) response=$(echo "$response" | sed '$d') - log_info "API: Response HTTP $http_code" + [[ "$http_code" != "200" ]] && { log_err "API HTTP $http_code"; return 1; } - if [[ "$http_code" != "200" ]]; then - log_error "API returned HTTP $http_code" - return 1 - fi - - # Check for GraphQL errors - if echo "$response" | jq -e '.errors' >/dev/null 2>&1; then - log_error "GraphQL error: $(echo "$response" | jq -r '.errors[0].message // "Unknown error"')" - return 1 - fi - - # Parse response - extract headings, clean HTML, and convert to slugs local results - results=$(echo "$response" | jq -r '.data.searchPage.flat.hits[].teaser | select(.heading != null) | .heading as $title | ($title | ascii_downcase | gsub("<[^>]*>"; "") | gsub("[^a-z0-9 ]"; ""; "g") | gsub("^ +| +$"; "") | gsub(" +"; "-")) as $slug | $title + "|" + $slug' 2>/dev/null) - local jq_ret=$? - - if [[ $jq_ret -ne 0 ]]; then - log_error "jq parsing failed (ret=$jq_ret)" - return 1 - fi - - if [[ -z "$results" ]]; then - log_warn "No results from search" - return 1 - fi - - local result_count=$(echo "$results" | grep -c '|') - log_info "API: Search returned $result_count results" + results=$(echo "$response" | jq -r ' + .data.searchPage.flat.hits[].teaser | + select(.heading != null and .item.urls.svtplay != null) | + (.heading | gsub("<[^>]*>"; "")) + "|" + (.item.urls.svtplay | ltrimstr("/")) + ' 2>/dev/null) + [[ -z "$results" ]] && return 1 + log "API" "Found $(echo "$results" | wc -l) results" echo "$results" } -# Get all active programs from Tablåtjänsten -get_popular_programs() { - log_info "API: Fetching popular programs" - - # Use GraphQL API with broad search query "a" to get popular/varied content (with longer timeout) - local response - response=$(curl -s --connect-timeout 10 --max-time 30 -w "\n%{http_code}" -X POST "https://api.svt.se/contento/graphql" \ - -H "Content-Type: application/json" \ - -d '{"query": "{ searchPage(query: \"a\", maxHits: 50) { flat { hits { teaser { heading } } } } }"}' 2>/dev/null) || { - log_error "API: Failed to query (curl error)" - return 1 - } - - local http_code=$(echo "$response" | tail -1) - response=$(echo "$response" | sed '$d') - - log_info "API: Popular programs response HTTP $http_code" - - if [[ "$http_code" != "200" ]]; then - log_error "API returned HTTP $http_code for popular programs" - return 1 - fi - - # Check response is not empty - local resp_len=$(echo "$response" | wc -c) - log_info "API: Response size: $resp_len bytes" - - if [[ $resp_len -lt 50 ]]; then - log_error "API: Response too small, likely error: $response" - return 1 - fi - - # Parse flat hits (popular programs) - # Format output: title|slug - local results - log_info "API: Parsing results with jq" - results=$(echo "$response" | jq -r '.data.searchPage.flat.hits[].teaser | select(.heading != null) | .heading as $title | ($title | ascii_downcase | gsub("<[^>]*>"; "") | gsub("[^a-z0-9 ]"; ""; "g") | gsub("^ +| +$"; "") | gsub(" +"; "-")) as $slug | $title + "|" + $slug' 2>/dev/null) - local jq_ret=$? - - if [[ $jq_ret -ne 0 ]]; then - log_error "jq parsing failed (ret=$jq_ret), response: $response" - return 1 - fi - - if [[ -z "$results" ]]; then - log_warn "API: jq returned empty results" - log_info "API: Full response: $response" - return 1 - fi - - local result_count=$(echo "$results" | grep -c '|') - log_info "API: Popular programs parsed $result_count results" - - echo "$results" -} - -# Show search dialog show_search_dialog() { - log_info "Showing search dialog" - - local search_term - search_term=$(zenity_dark --entry \ - --title="Search SVT Play" \ - --text="Enter program name to search:" \ - --width=500 \ - 2>/dev/null) - - if [[ -z "$search_term" ]]; then - log_debug "Search cancelled" - return 1 - fi - - log_debug "Search term: $search_term" - echo "$search_term" + zenity_dark --entry --title="Search SVT Play" --text="Program name:" --width=400 2>/dev/null } -# Show results in zenity list show_results_list() { - local results="$1" - log_debug "Showing results list" + local results="$1" temp_file="/tmp/svt_$$" - if [[ -z "$results" ]]; then - log_warn "No results provided" - zenity_dark --error --title="No Results" --text="No programs found." - return 1 - fi - - local result_count=$(echo "$results" | wc -l) - log_debug "Found $result_count results" - - # Create temp file with formatted list for zenity - local temp_file="/tmp/svt_search_$$.txt" + # Build zenity list and mapping file local idx=0 - while IFS='|' read -r title slug; do [[ -z "$title" || -z "$slug" ]] && continue ((idx++)) - - # Clean HTML tags - title=$(echo "$title" | sed 's/<[^>]*>//g' | sed 's/&/\&/g' | sed 's/"/"/g' | sed "s/'/'/g" | sed 's/<//g') - - # Save title->slug mapping + title=$(echo "$title" | sed 's/&/\&/g; s/"/"/g; s/<//g') echo "$title=$slug" >> "$temp_file.map" - - if [[ $idx -eq 1 ]]; then - echo "TRUE" >> "$temp_file" - else - echo "FALSE" >> "$temp_file" - fi + [[ $idx -eq 1 ]] && echo "TRUE" >> "$temp_file" || echo "FALSE" >> "$temp_file" echo "$title" >> "$temp_file" done <<< "$results" [[ $idx -eq 0 ]] && { rm -f "$temp_file"*; return 1; } - log_debug "Built list with $idx items" - - # Display zenity dialog local selected - selected=$(zenity_dark --list --title="Search Results" --text="Select a program:" --column="Select" --column="Program" --radiolist --width=700 --height=500 < "$temp_file" 2>/dev/null) - - if [[ -z "$selected" ]]; then - log_debug "User cancelled" - rm -f "$temp_file"* - return 1 - fi - - # Look up slug - local slug - slug=$(grep "^$selected=" "$temp_file.map" 2>/dev/null | cut -d= -f2) + selected=$(zenity_dark --list --title="Results" --text="Select program:" \ + --column="Select" --column="Program" --radiolist --width=700 --height=500 < "$temp_file" 2>/dev/null) + local slug="" + [[ -n "$selected" ]] && slug=$(grep "^${selected}=" "$temp_file.map" 2>/dev/null | cut -d= -f2) rm -f "$temp_file"* - if [[ -z "$slug" ]]; then - log_error "Could not map selection to slug" - return 1 - fi - - log_success "Selected: $selected" + [[ -z "$slug" ]] && return 1 + log "✓" "Selected: $selected" echo "$slug" } -# Show popular programs -show_popular_programs() { - log_info "Fetching popular programs from SVT Play..." - - # Get programs (API call takes 10-20 seconds) - local programs - programs=$(get_popular_programs) - - if [[ -z "$programs" ]]; then - log_error "No popular programs fetched" - zenity_dark --error --title="Error" --text="Failed to fetch popular programs from SVT Play." - return 1 - fi - - log_info "Got $(echo "$programs" | wc -l) results, preparing list..." - - # Create temp file with formatted list for zenity - local temp_file="/tmp/svt_list_$$.txt" - local idx=0 - local first_title="" - - while IFS='|' read -r title slug; do - [[ -z "$title" || -z "$slug" ]] && continue - ((idx++)) - - # Clean HTML tags - title=$(echo "$title" | sed 's/<[^>]*>//g' | sed 's/&/\&/g' | sed 's/"/"/g' | sed "s/'/'/g" | sed 's/<//g') - - # Save title->slug mapping - echo "$title=$slug" >> "$temp_file.map" - - if [[ $idx -eq 1 ]]; then - echo "TRUE" >> "$temp_file" - first_title="$title" - else - echo "FALSE" >> "$temp_file" - fi - echo "$title" >> "$temp_file" - done <<< "$programs" - - [[ $idx -eq 0 ]] && { log_error "No valid items"; rm -f "$temp_file"*; return 1; } - - log_info "Built list with $idx items" - - # Display zenity dialog with the list - log_info "Showing zenity list dialog..." - local selected - selected=$(zenity_dark --list --title="Popular Programs" --text="Select a program to stream:" --column="Select" --column="Program" --radiolist --width=700 --height=600 < "$temp_file" 2>/dev/null) - - local ret=$? - - if [[ $ret -ne 0 ]] || [[ -z "$selected" ]]; then - log_debug "User cancelled or dialog failed (ret=$ret)" - rm -f "$temp_file"* - return 1 - fi - - # Look up slug from the selected title - local slug - slug=$(grep "^$selected=" "$temp_file.map" 2>/dev/null | cut -d= -f2) - - rm -f "$temp_file"* - - if [[ -z "$slug" ]]; then - log_error "Could not find slug for: $selected" - return 1 - fi - - log_success "Selected: $selected" - echo "$slug" -} - -# Show main menu -show_main_menu() { - log_info "Showing main menu" - - local choice - choice=$(zenity_dark --list \ - --title="SVT Play Stream Launcher" \ - --text="Select an option:" \ - --column="Option" \ - "Search for Program" \ - "Browse Popular Programs" \ - --width=400 \ - --height=200 \ - 2>/dev/null) - - if [[ -z "$choice" ]]; then - log_debug "User cancelled main menu" - return 1 - fi - - echo "$choice" -} - -# Play program directly play_program() { - local slug="$1" - local url="https://www.svtplay.se/$slug" - - log_info "Playing program: $slug" - log_debug "URL: $url" - - # Verify URL is accessible - if ! yt-dlp --quiet --dump-json "$url" &>/dev/null; then - log_error "Failed to access program: $slug" - zenity_dark --error --title="Error" --text="Failed to access program. It may not be available." - return 1 - fi - - log_success "Starting playback for: $slug" - - # Launch mpv with yt-dlp integration + local url="https://www.svtplay.se/$1" + log "▶" "Playing: $url" mpv --force-window=immediate "$url" & - local pid=$! - - log_success "Playback started (PID: $pid)" - - if [[ "$WAIT_FOR_MPV" == "1" ]]; then - log_info "Waiting for mpv to finish..." - wait $pid 2>/dev/null - log_success "Playback finished" - else - log_info "Playback continues in background" - fi + [[ "$WAIT_FOR_MPV" == "1" ]] && wait $! 2>/dev/null } -# Main function main() { - log_info "═════ SVT Play Stream Launcher started ═════" - log_info "PID: $$, DIR: $PWD" - check_dependencies - # Show main menu - local menu_choice - menu_choice=$(show_main_menu) || exit 0 + local search_term + search_term=$(show_search_dialog) || exit 0 + [[ -z "$search_term" ]] && exit 0 - local program_slug + local results + results=$(search_programs "$search_term") || { + zenity_dark --error --title="No Results" --text="No programs found for: $search_term" + exit 0 + } - case "$menu_choice" in - "Search for Program") - local search_term - search_term=$(show_search_dialog) || exit 0 - - log_info "Searching for: $search_term" - - # Get search results (API call takes 10-20 seconds) - search_results=$(search_programs "$search_term") - - if [[ -z "$search_results" ]]; then - zenity_dark --error --title="No Results" --text="No programs found for: $search_term" - exit 0 - fi - - program_slug=$(show_results_list "$search_results") || exit 0 - ;; - "Browse Popular Programs") - program_slug=$(show_popular_programs) || exit 0 - ;; - *) - log_error "Invalid menu choice: $menu_choice" - exit 1 - ;; - esac + local slug + slug=$(show_results_list "$results") || exit 0 - # Play the selected program - play_program "$program_slug" - - log_success "Session complete" - log_info "═════════════════════════════════════════════════════" + play_program "$slug" } -# Only run main if script is executed directly (not sourced) [[ "${BASH_SOURCE[0]}" == "${0}" ]] && main "$@"