#!/bin/bash # SVT Play Stream Launcher - 100% GUI Based # Search and stream SVT Play content with zenity interface 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 while [[ $# -gt 0 ]]; do case "$1" in --debug) DEBUG=1; shift ;; --wait) WAIT_FOR_MPV=1; shift ;; --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 ;; *) 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' # Zenity dark mode helper zenity_dark() { GTK_THEME=Adwaita: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 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[*]}" 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" # 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 http_code=$(echo "$response" | tail -1) response=$(echo "$response" | sed '$d') log_info "API: Response HTTP $http_code" 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" 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" } # Show results in zenity list show_results_list() { local results="$1" log_debug "Showing results list" 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" 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 echo "$title=$slug" >> "$temp_file.map" if [[ $idx -eq 1 ]]; then echo "TRUE" >> "$temp_file" else echo "FALSE" >> "$temp_file" fi 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) rm -f "$temp_file"* if [[ -z "$slug" ]]; then log_error "Could not map selection to slug" return 1 fi log_success "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 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 } # 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 program_slug 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 # Play the selected program play_program "$program_slug" log_success "Session complete" log_info "═════════════════════════════════════════════════════" } # Only run main if script is executed directly (not sourced) [[ "${BASH_SOURCE[0]}" == "${0}" ]] && main "$@"