upload
This commit is contained in:
458
svt.sh
Executable file
458
svt.sh
Executable file
@@ -0,0 +1,458 @@
|
||||
#!/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' | 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' | 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 "$@"
|
||||
Reference in New Issue
Block a user