#!/usr/bin/env bash ## Definitions GRPOPPRO_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/grpoppro" GRPOPPRO_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/grpoppro" GRPOPPRO_CONFIG_FILE="$GRPOPPRO_CONFIG_DIR/grpopprorc" GRPOPPRO_DATA_FILE="$GRPOPPRO_DATA_DIR/lastPlayedSeries" GRPOPPRO_PLAYLIST_FILE="$GRPOPPRO_DATA_DIR/lastPlaylistPlayed" GRPOPPRO_COOKIE_FILE="$GRPOPPRO_DATA_DIR/cookies.txt" GRPOPPRO_HISTORY_FILE="$GRPOPPRO_DATA_DIR/history" ### Curl constants USER_AGENT="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0" ACCEPT="Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" ACCEPT_LANGUAGE="Accept-Language: en-US,en;q=0.5" CONNECTION="Connection: keep-alive" UP_IN_REQ="Upgrade-Insecure-Requests: 1" ### End of curl constants MPV_OPTS='--vo=gpu \ --gpu-api=opengl \ --opengl-es=yes \ --msg-level=all=no \ --really-quiet \ --profile=sw-fast \ --no-ytdl \ --http-header-fields="User-Agent: '"$USER_AGENT"'" \ --fs' ## End of definitions # Display a message function message { local interactive="$GRPOPPRO_INTERACTIVE" local msg="$1" if [[ "$interactive" == "on" ]]; then notify-send -t 2000 "$msg" else echo -e "\n\t$msg" fi } # Standard curl request function curlRequest { local url="$1" local custom_method="${2:-}" local custom_accept="${3:-}" local shifts=1 # Initialize shifts to account for the URL shift # Shift out the URL # Check for custom method and Accept header, and shift accordingly if [[ -n "$custom_method" ]]; then shifts=$((shifts + 1)) shift fi if [[ -n "$custom_accept" ]]; then shifts=$((shifts + 1)) shift fi # Apply default values if not provided custom_method="${custom_method:-GET}" custom_accept="${custom_accept:-$ACCEPT}" # Dynamically shift the total number of times needed shift $((shifts - 1)) if [[ "$GRPOPPRO_DEBUG" == "off" ]]; then if [[ "$OSTYPE" == "linux-gnu" ]]; then response="$(curl_chrome131 -s -L \ -c"$GRPOPPRO_COOKIE_FILE" \ -b"$GRPOPPRO_COOKIE_FILE" \ "$url" \ --compressed \ -X "$custom_method" \ -A "$USER_AGENT" \ -H "$custom_accept" \ -H "$ACCEPT_LANGUAGE" \ -H "$CONNECTION" \ -H "$UP_IN_REQ" \ "$@" )" else response="$(curl -s -L \ -c "$GRPOPPRO_COOKIE_FILE" \ -b "$GRPOPPRO_COOKIE_FILE" \ "$url" \ --compressed \ -X "$custom_method" \ -A "$USER_AGENT" \ -H "$custom_accept" \ -H "$ACCEPT_LANGUAGE" \ -H "$CONNECTION" \ -H "$UP_IN_REQ" \ "$@" )" fi else if [[ "$OSTYPE" == "linux-gnu" ]]; then LOG_FILE="/tmp/grpoppro_curl_commands.log" local curl_command=( "curl_chrome131" "-s" "-L" "-c$GRPOPPRO_COOKIE_FILE" "-b$GRPOPPRO_COOKIE_FILE" "$url" "-X" "$custom_method" "-H $custom_accept" "$@" ) else LOG_FILE="$GRPOPPRO_DATA_DIR/grpoppro_curl_commands.log" local curl_command=( "curl" "-s" "-L" "-c $GRPOPPRO_COOKIE_FILE" "-b $GRPOPPRO_COOKIE_FILE" "$url" "--compressed" "-X" "$custom_method" "-A $USER_AGENT" "-H $custom_accept" "-H $ACCEPT_LANGUAGE" "-H $CONNECTION" "-H $UP_IN_REQ" "$@" ) fi { echo -e "<===========================\n" echo "Called by: ${FUNCNAME[1]}" echo "" echo -e "$(date '+%d-%m-%Y %H:%M:%S') -> ${curl_command[5]}\n" printf "%s\n" "${curl_command[@]}" echo "" echo -e "===========================>\n" } >> "$LOG_FILE" response=$("${curl_command[@]}") fi exit_code="$?" if [[ "$exit_code" -ne 0 ]]; then message "Error: Failed to execute curl request." exit 1 fi echo "$response" } # Print usage information function usage { echo -e "\n\tUsage:\n\n\t""$(basename "$0")"" \"name of film or show\" [season] [episode(ex 01 02 .. 10 11)]" echo -e "\tOr ""$(basename "$0")"" [--menu|--resume|--select|--finish|--history]" echo -e "\tOr ""$(basename "$0")"" --open \"name of film or show\"" exit 0 } # Ensure fzf is installed for Termux function getFzfForTermux { if ! command -v fzf >/dev/null 2>&1; then apt install -y fzf fi } # Retrieve IMDb ID function getIMDBID { local title="$1" local response local imdbid response="$(curlRequest "https://www.imdb.com/find/?q=$title")" imdbid="$(echo "$response" \ | grep -io ' href=['"'"'"][^"'"'"']*['"'"'"]' \ | grep 'href="/title/' \ | head -n1 \ | sed -e 's/href="\/title\///' -e 's/\/?.*//' -e 's/^ //' )" # response="$(curlRequest "http://imdb.konsthol.eu/find?q=$title")" # imdbid="$(echo "$response" \ # | grep -io ' href=['"'"'"][^"'"'"']*['"'"'"]' \ # | grep title | head -n1 \ # | sed -e 's/^ //' -e 's/href="\/title\///' -e 's/"//')" if ! [[ "$imdbid" == *"tt"* ]]; then message "Film or show not found in imdb" exit 0 fi echo "$imdbid" } # Prepare basic steps for a request function basics { local title="$1" local imdbid local simpleurl title="${title// /%20}" imdbid="$(getIMDBID "$title")" simpleurl="$apiurl/embed/$imdbid/" echo "$simpleurl" } # Retrieve internal ID function getInternalID { local simpleurl="$1" local response local internalid response="$(curlRequest "$simpleurl")" if echo "$response" | grep -q "404 Not Found"; then message "Non valid response" exit 0 fi if echo "$response" | grep -q "id='el-content'>"; then message "No file found" exit 0 fi internalid="$(echo "$response" \ | grep news_id \ | awk '{print $5}' \ | awk -F\' '{print $2}' )" echo "$internalid" } # Retrieve movie stream URL function getMovieStreamUrl { local internalid="$1" local PHPSESSID PHPSESSID="$(grep 'PHPSESSID' "$GRPOPPRO_COOKIE_FILE" | awk '{print $NF}')" local response local streamurl local headers=( '-H Accept-Encoding: gzip, deflate, br, zstd' '-H Content-Type: application/x-www-form-urlencoded; charset=UTF-8' '-H X-Requested-With: XMLHttpRequest' "-H Origin: $apiurl" '-H DNT: 1' '-H Sec-GPC: 1' "-H Referer: $simpleurl" "-H Cookie: PHPSESSID=$PHPSESSID" '-H Sec-Fetch-Dest: empty' '-H Sec-Fetch-Mode: cors' '-H Sec-Fetch-Site: same-origin' '-H TE: trailers' '--data-raw' "mod=players&news_id=$internalid" ) response="$(curlRequest "$apiurl/engine/ajax/controller.php" \ "POST" \ 'Accept: application/json, text/javascript, */*; q=0.01' \ "${headers[@]}" )" streamurl="$(echo "$response" \ | sed 's/\\//g' \ | grep -oP 'file:"\K[^"]+' )" echo "$streamurl" } # Stream the video function play { local streamurl="$1" if [[ "$player" == "mpv" ]]; then MPVPID="$(pidof mpv | cut -d' ' -f1)" [[ -n "$MPVPID" ]] && kill -s SIGTERM "$MPVPID" player="mpv --save-position-on-quit ${MPV_OPTS}" fi [[ "$OSTYPE" != "linux-gnu" ]] && GRPOPPRO_INTERACTIVE="off" && streamurl="${streamurl//https/http}" # No setsid -f here because we need to wait for mpv to finish eval "$player" "$streamurl" } # Dump data to files function dumpData { local title="$1" local seasonEpisode="$2" local streamurl="$3" echo "$title" > "$GRPOPPRO_DATA_FILE" echo "$seasonEpisode" >> "$GRPOPPRO_DATA_FILE" echo "$streamurl" >> "$GRPOPPRO_DATA_FILE" message "Last Played -> $seasonEpisode" echo "$title|$seasonEpisode|$streamurl" >> "$GRPOPPRO_HISTORY_FILE" tac "$GRPOPPRO_HISTORY_FILE" | awk -F'|' '!seen[$1]++' | tac > "${GRPOPPRO_DATA_DIR}/temp.txt" && mv "${GRPOPPRO_DATA_DIR}/temp.txt" "$GRPOPPRO_HISTORY_FILE" } # Source data from files function sourceData { local lastSeriesPlayed local lastSeasonEpisodePlayed local lastEpisodePlayedURL if [[ -f "$GRPOPPRO_DATA_FILE" ]]; then lastSeriesPlayed="$(sed -n 1p "$GRPOPPRO_DATA_FILE")" lastSeasonEpisodePlayed="$(sed -n 2p "$GRPOPPRO_DATA_FILE")" lastEpisodePlayedURL="$(sed -n 3p "$GRPOPPRO_DATA_FILE")" fi echo "$lastSeriesPlayed|$lastSeasonEpisodePlayed|$lastEpisodePlayedURL" } # Perform menu search function menuSearch { local title local imdbid local simpleurl local internalid local response local seasonEpisode local streamurl # Two requests to the api if [[ "$GRPOPPRO_INTERACTIVE" == "on" ]]; then title=$(echo "" | wofi --dmenu --insensitive --prompt="Search Series" --width=300 --height=50) else read -r -p "Search Series: " title fi if [[ -z "$title" ]]; then message "Nothing given" exit 0 fi title="${title// /%20}" imdbid="$(getIMDBID "$title")" simpleurl="$apiurl/embed/$imdbid/" internalid="$(getInternalID "$simpleurl")" response="$(curlRequest "$apiurl/uploads/playlists/$internalid.txt")" echo "$response" > "$GRPOPPRO_PLAYLIST_FILE" title="$(grep "mp4" "$GRPOPPRO_PLAYLIST_FILE" \ | head -n1 \ | awk -F/ '{print $(NF-1)}' )" title="${title//_/ }" title="${title//-/ }" seasonEpisode="$(jq '.[]' "$GRPOPPRO_PLAYLIST_FILE" \ | grep "mp4" \ | awk -F/ '{print $NF}' \ | awk -F. '{print $1}' \ | eval "$menu"\""$title" \" )" if [[ -z "$seasonEpisode" ]]; then message "Nothing Selected" exit 0 fi streamurl="$(jq '.[]' "$GRPOPPRO_PLAYLIST_FILE" \ | grep "mp4" \ | grep "${seasonEpisode}" \ | head -n1 \ | awk -F\" '{print $4}' )" dumpData "$title" "$seasonEpisode" "$streamurl" play "$streamurl" resume } # Resume last watched series function resume { # Zero requests to the api if [[ ! -f "$GRPOPPRO_DATA_FILE" ]]; then message "Nothing to resume" exit 0 fi local data local title data="$(sourceData)" title="$(echo "$data" | cut -d '|' -f1)" while true do data="$(sourceData)" local seasonEpisode local lastSeasonEpisodePlayed lastSeasonEpisodePlayed="$(echo "$data" | cut -d '|' -f2)" episodes_left="$(jq '.[]' "$GRPOPPRO_PLAYLIST_FILE" \ | grep "mp4" \ | awk -F/ '{print $NF}' \ | awk -F. '{print $1}' \ | sed -n -e "0,/$lastSeasonEpisodePlayed/!p" )" if [[ -z "$episodes_left" ]]; then message "You finished $title" exit 0 fi seasonEpisode="$(printf "%s" "$episodes_left" | eval "$menu"\""$title" \")" if [[ -z "$seasonEpisode" ]]; then message "Nothing Selected" exit 0 fi local streamurl streamurl="$(jq '.[]' "$GRPOPPRO_PLAYLIST_FILE" \ | grep "${seasonEpisode}" \ | head -n1 \ | awk -F\" '{print $4}' )" dumpData "$title" "$seasonEpisode" "$streamurl" play "$streamurl" done } # Select which series to resume function resumeSeries { # Zero requests to the api if it's the last series you watched # Two requests to the api otherwise if [[ ! -f "$GRPOPPRO_HISTORY_FILE" ]]; then message "No history found" exit 0 fi local resume_series resume_series="$(awk -F'|' '{print $1}' "$GRPOPPRO_HISTORY_FILE" \ | sort \ | uniq \ | eval "$menu"\"Select Series \" )" if [[ -z "$resume_series" ]]; then message "No series selected" exit 0 fi local resume_episode resume_episode="$(grep "^$resume_series|" "$GRPOPPRO_HISTORY_FILE" | awk -F'|' '{print $2}')" if [[ -n "$resume_episode" ]]; then message "Continuing $resume_series from $resume_episode" fi local resume_streamurl resume_streamurl=$(grep "^$resume_series|$resume_episode|" "$GRPOPPRO_HISTORY_FILE" | awk -F'|' '{print $3}') local title="$resume_series" local lastTitle lastTitle="$(sed -n 1p "$GRPOPPRO_DATA_FILE")" if [[ ! "$title" == "$lastTitle" ]]; then title="${title// /%20}" imdbid="$(getIMDBID "$title")" simpleurl="$apiurl/embed/$imdbid/" internalid="$(getInternalID "$simpleurl")" response="$(curlRequest "$apiurl/uploads/playlists/$internalid.txt")" echo "$response" > "$GRPOPPRO_PLAYLIST_FILE" fi title="$(grep "mp4" "$GRPOPPRO_PLAYLIST_FILE" | head -n1 | awk -F/ '{print $(NF-1)}')" title="${title//_/ }" title="${title//-/ }" local seasonEpisode="$resume_episode" local streamurl="$resume_streamurl" dumpData "$title" "$seasonEpisode" "$streamurl" play "$streamurl" resume } # Resume last watched unfinished episode function resumeUnfinishedEpisode { # Zero requests to the api if [[ ! -f "$GRPOPPRO_DATA_FILE" ]]; then message "Nothing to resume" exit 0 fi local data local lastSeriesPlayed local lastSeasonEpisodePlayed local lastEpisodePlayedURL data="$(sourceData)" lastSeriesPlayed="$(echo "$data" | cut -d '|' -f1)" lastSeasonEpisodePlayed="$(echo "$data" | cut -d '|' -f2)" lastEpisodePlayedURL="$(echo "$data" | cut -d '|' -f3)" message "Continuing $lastSeriesPlayed from $lastSeasonEpisodePlayed" local streamurl="$lastEpisodePlayedURL" if [[ "$player" == "mpv" ]]; then MPVPID="$(pidof mpv | cut -d' ' -f1)" [[ -n "$MPVPID" ]] && kill -s SIGTERM "$MPVPID" player="mpv --resume-playback ${MPV_OPTS}" fi [[ "$OSTYPE" != "linux-gnu" ]] && GRPOPPRO_INTERACTIVE="off" && streamurl="${streamurl//https/http}" # No setsid -f here because we need to wait for mpv to finish eval "$player" "$streamurl" resume } # Open in browser to watch function openInBrowser { local title="$2" local simpleurl # One request to the api simpleurl="$(basics "$title")" local internalid internalid="$(getInternalID "$simpleurl")" xdg-open "$simpleurl" exit 0 } function getHistory { if [[ ! -f "$GRPOPPRO_HISTORY_FILE" ]]; then message "No history found" exit 0 fi local history history="$(awk -F'|' '{print $1,":<=> At =>",$2}' "$GRPOPPRO_HISTORY_FILE" | column -t -s':')" message "$history" } function getSeries { local title="$1" local season="$2" local episode="$3" local simpleurl simpleurl="$(basics "$title")" local internalid internalid="$(getInternalID "$simpleurl")" local response # Two requests to the api response="$(curlRequest "$apiurl/uploads/playlists/$internalid.txt")" local streamurl streamurl="$(echo "$response" \ | jq '.[]' \ | grep "mp4" \ | grep "${season}x${episode}" \ | head -n1 \ | awk -F\" '{print $4}' )" title="${title//%20/ }" local capitalized_title capitalized_title="$(echo "$title" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1')" message "Loading $capitalized_title at season $season on episode $episode" play "$streamurl" exit 0 } function getMovie { local simpleurl # Two requests to the api if [[ "$1" == *"tt"* ]]; then simpleurl="$1" else simpleurl="$(basics "$1")" # Gets the title, performs one request to imdb and gets the complete url fi local internalid internalid="$(getInternalID "$simpleurl")" # Gets the complete url and performs one request to get the internal id or inform us it does not exist local streamurl streamurl="$(getMovieStreamUrl "$internalid")" # Gets the internal id and performs one request to get the stream url local title="${title//%20/ }" local capitalized_title capitalized_title="$(echo "$title" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1')" message "Loading $capitalized_title" play "$streamurl" # It uses mpv to stream the file exit 0 } ### Program starts here ### # Prepare the necessary steps for Termux environment if [[ "$OSTYPE" != "linux-gnu" ]]; then GRPOPPRO_INTERACTIVE="off" menu="fzf -i --prompt=seasonXepisode" player="am start -n is.xyz.mpv/.MPVActivity -a android.intent.action.VIEW -d" getFzfForTermux fi if [[ -f "$GRPOPPRO_CONFIG_FILE" ]]; then while IFS='=' read -r key value; do # Ignore lines starting with # if [[ "$key" =~ ^#.* ]]; then continue fi # Check if value starts and ends with quotes if [[ $value =~ ^\".*\"$ ]]; then # Remove surrounding quotes, but preserve internal spaces value="${value%\"}" # Remove trailing quote value="${value#\"}" # Remove leading quote else # Wont appear for empty config file # But will appear for empty lines echo "Invalid value found" exit 1 fi export "$key=$value" done < "$GRPOPPRO_CONFIG_FILE" else echo "Configuration file not found!" echo "Creating one at $GRPOPPRO_CONFIG_FILE" mkdir -p "$GRPOPPRO_CONFIG_DIR" # touch "$GRPOPPRO_CONFIG_FILE" DEFAULTS=$(cat < "$GRPOPPRO_CONFIG_FILE" echo "Try again" exit 1 fi # Set default values if not set in config : "${GRPOPPRO_INTERACTIVE:=on}" : "${GRPOPPRO_DEBUG:=on}" : "${apiurl:=https://coverapi.store}" : "${player:=mpv}" : "${menu:=fuzzel -d -p }" # Ensure curl-impersonate is installed if [[ "$OSTYPE" == "linux-gnu" ]]; then if ! command -v curl-impersonate-chrome >/dev/null 2>&1; then message "You should get curl-impersonate to make it seem less obvious you use curl" exit 0 fi fi # Make sure the data directory exists mkdir -p "$GRPOPPRO_DATA_DIR" # Exit if a VPN connection is active if [[ "$OSTYPE" == "linux-gnu" ]]; then if nmcli connection show --active | grep -i -q wireguard; then message "Exiting because a vpn connection is active" exit 0 fi fi [[ "$#" -lt 1 ]] && usage [[ "$#" -gt 3 ]] && usage [[ "$#" -eq 3 ]] && getSeries "$@" [[ "$#" -eq 1 ]] \ && [[ "$1" != "--open" ]] \ && [[ "$1" != "--menu" ]] \ && [[ "$1" != "--resume" ]] \ && [[ "$1" != "--finish" ]] \ && [[ "$1" != "--select" ]] \ && [[ "$1" != "--history" ]] \ && getMovie "$@" [[ "$1" == "--open" ]] && openInBrowser "$@" [[ "$1" == "--menu" ]] && menuSearch [[ "$1" == "--resume" ]] && resume [[ "$1" == "--finish" ]] && resumeUnfinishedEpisode [[ "$1" == "--select" ]] && resumeSeries [[ "$1" == "--history" ]] && getHistory exit 0