| @@ -0,0 +1,204 @@ | |||
| 1 | + | #!/bin/bash | |
| 2 | + | # ============================================================================= | |
| 3 | + | # ingestEpisode.sh | |
| 4 | + | # ============================================================================= | |
| 5 | + | # Descripció: | |
| 6 | + | # Script unificat per al processament d'episodis de podcast. | |
| 7 | + | # S'executa automàticament des de FlexGet un cop finalitzada la descàrrega. | |
| 8 | + | # | |
| 9 | + | # Flux d'execució: | |
| 10 | + | # 1. Processa l'arxiu d'àudio (metadades + portada via ffmpeg) | |
| 11 | + | # 2. Regenera el feed RSS del podcast corresponent | |
| 12 | + | # 3. Envia una notificació NTFY amb el resum de l'operació | |
| 13 | + | # | |
| 14 | + | # Ús: | |
| 15 | + | # ./ingestEpisode.sh "YYYY-MM-DD" "Títol" "Autor" "/ruta/base/podcast" | |
| 16 | + | # | |
| 17 | + | # Paràmetres: | |
| 18 | + | # $1 rss_pubdate Data de publicació de l'episodi (format: YYYY-MM-DD) | |
| 19 | + | # $2 titol Títol de l'episodi (també s'usa com a nom d'arxiu) | |
| 20 | + | # $3 autor Autor / nom del podcast | |
| 21 | + | # $4 path Ruta absoluta al directori base del podcast | |
| 22 | + | # (ex: /data/podcast/NomDelPodcast) | |
| 23 | + | # | |
| 24 | + | # Dependències: | |
| 25 | + | # - ffmpeg Processament d'àudio i incrustació de metadades | |
| 26 | + | # - curl Enviament de notificacions a NTFY | |
| 27 | + | # - find / sort Generació del feed RSS | |
| 28 | + | # | |
| 29 | + | # Arxius esperats al directori base ($path): | |
| 30 | + | # - YYYYMMDD.mp3 o YYYYMMDD.m4a Arxiu d'àudio descarregat per FlexGet | |
| 31 | + | # - cover.jpg Portada del podcast (opcional) | |
| 32 | + | # - .ntfyToken Token d'autenticació NTFY (a /config/) | |
| 33 | + | # | |
| 34 | + | # Sortida: | |
| 35 | + | # - $path/$titol/$titol.mp3|m4a Arxiu processat amb metadades i portada | |
| 36 | + | # - $path/$titol/cover.jpg Còpia de la portada (si existeix) | |
| 37 | + | # - /data/podcast/$DIR_NAME.xml Feed RSS regenerat | |
| 38 | + | # | |
| 39 | + | # Configuració: | |
| 40 | + | # Les constants de la secció "Config RSS & NTFY" s'han d'adaptar | |
| 41 | + | # si canvia el servidor, el domini o l'estructura de directoris. | |
| 42 | + | # | |
| 43 | + | # ============================================================================= | |
| 44 | + | ||
| 45 | + | set -e | |
| 46 | + | ||
| 47 | + | rss_pubdate="$1" | |
| 48 | + | titol="$2" | |
| 49 | + | autor="$3" | |
| 50 | + | path="$4" | |
| 51 | + | ||
| 52 | + | # --- Config RSS & NTFY ----------------------------------------------------------- | |
| 53 | + | # BASE_DIR: directori arrel on resideixen tots els podcasts | |
| 54 | + | # OUTPUT_DIR: on es desen els feeds XML generats (normalment el mateix que BASE_DIR) | |
| 55 | + | # LINK_BASE: URL pública base des d'on s'accedeix als arxius | |
| 56 | + | # NTFY_SERVER: URL interna del contenidor NTFY | |
| 57 | + | # NTFY_TOPIC: tema NTFY on s'envien les notificacions | |
| 58 | + | BASE_DIR="/data/podcast" | |
| 59 | + | OUTPUT_DIR="/data/podcast" | |
| 60 | + | LINK_BASE="https://podcast.tuServidor.org" | |
| 61 | + | NTFY_SERVER="http://ntfy:80" | |
| 62 | + | NTFY_TOPIC="podcast-ingest" | |
| 63 | + | NTFY_TOKEN="$(cat /config/.ntfyToken | tr -d '\r\n')" | |
| 64 | + | ||
| 65 | + | [ -z "$NTFY_TOKEN" ] && { echo "Token NTFY vacio"; exit 1; } | |
| 66 | + | ||
| 67 | + | # --- Fechas ---------------------------------------------------------------------- | |
| 68 | + | # pubdateYmd: format compacte per localitzar l'arxiu descarregat (ex: 20250728) | |
| 69 | + | # pubdateY: any per a la metadada 'date' de l'arxiu d'àudio (ex: 2025) | |
| 70 | + | pubdateYmd=$(date -d "$rss_pubdate" +"%Y%m%d") | |
| 71 | + | pubdateY=$(date -d "$rss_pubdate" +"%Y") | |
| 72 | + | ||
| 73 | + | cover="$path/cover.jpg" | |
| 74 | + | destFolder="$path/$titol" | |
| 75 | + | ||
| 76 | + | mkdir -p "$destFolder" | |
| 77 | + | ||
| 78 | + | # --- Procesar audio -------------------------------------------------------------- | |
| 79 | + | # Incrusta metadades ID3 i portada a l'arxiu d'àudio mitjançant ffmpeg. | |
| 80 | + | # Si existeix cover.jpg al directori base, s'afegeix com a attached_pic. | |
| 81 | + | # L'arxiu original es elimina un cop processat correctament. | |
| 82 | + | processFile () { | |
| 83 | + | local input="$1" | |
| 84 | + | local ext="$2" | |
| 85 | + | local output="$destFolder/$titol.$ext" | |
| 86 | + | ||
| 87 | + | if [[ -f "$cover" ]]; then | |
| 88 | + | ffmpeg -y -loglevel quiet -i "$input" \ | |
| 89 | + | -i "$cover" \ | |
| 90 | + | -map 0:a -map 1:v \ | |
| 91 | + | -map_metadata -1 \ | |
| 92 | + | -c copy \ | |
| 93 | + | -metadata title="$titol" \ | |
| 94 | + | -metadata artist="$autor" \ | |
| 95 | + | -metadata album="$titol" \ | |
| 96 | + | -metadata genre="Podcast" \ | |
| 97 | + | -metadata track="1" \ | |
| 98 | + | -metadata date="$pubdateY" \ | |
| 99 | + | -disposition:v:0 attached_pic \ | |
| 100 | + | "$output" | |
| 101 | + | else | |
| 102 | + | ffmpeg -y -loglevel quiet -i "$input" \ | |
| 103 | + | -map 0:a \ | |
| 104 | + | -c copy \ | |
| 105 | + | -metadata title="$titol" \ | |
| 106 | + | -metadata artist="$autor" \ | |
| 107 | + | -metadata album="$titol" \ | |
| 108 | + | -metadata genre="Podcast" \ | |
| 109 | + | -metadata track="1" \ | |
| 110 | + | -metadata date="$pubdateY" \ | |
| 111 | + | "$output" | |
| 112 | + | fi | |
| 113 | + | ||
| 114 | + | rm -f "$input" | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | # Cerca l'arxiu descarregat pel nom de data. Si no existeix cap dels dos formats | |
| 118 | + | # suportats (mp3, m4a), el script finalitza sense error ni notificació. | |
| 119 | + | if [[ -f "$path/${pubdateYmd}.mp3" ]]; then | |
| 120 | + | processFile "$path/${pubdateYmd}.mp3" "mp3" | |
| 121 | + | elif [[ -f "$path/${pubdateYmd}.m4a" ]]; then | |
| 122 | + | processFile "$path/${pubdateYmd}.m4a" "m4a" | |
| 123 | + | else | |
| 124 | + | exit 0 | |
| 125 | + | fi | |
| 126 | + | ||
| 127 | + | # Copia la portada al directori de l'episodi per si el client RSS la necessita | |
| 128 | + | [[ -f "$cover" ]] && cp "$cover" "$destFolder/" | |
| 129 | + | ||
| 130 | + | # --- Generar feed RSS del podcast descargado ------------------------------------ | |
| 131 | + | # Regenera únicament el feed del podcast que acaba de rebre un episodi nou. | |
| 132 | + | # Recorre tots els arxius mp3/m4a del directori i els ordena per data (més recent primer). | |
| 133 | + | # La URL de cada arxiu s'escapa per reemplaçar espais per %20. | |
| 134 | + | DIR_NAME=$(basename "$path") | |
| 135 | + | OUTPUT_FILE="$OUTPUT_DIR/${DIR_NAME}.xml" | |
| 136 | + | TITLE="Podcast: ${DIR_NAME}" | |
| 137 | + | LINK="${LINK_BASE}/${DIR_NAME}.xml" | |
| 138 | + | IMAGE_URL="${LINK_BASE}/${DIR_NAME}/cover.jpg" | |
| 139 | + | NOW=$(date -R) | |
| 140 | + | ||
| 141 | + | { | |
| 142 | + | echo "<?xml version='1.0' encoding='UTF-8'?>" | |
| 143 | + | echo "<rss version='2.0' xmlns:itunes='http://www.itunes.com/dtds/podcast-1.0.dtd'>" | |
| 144 | + | echo "<channel>" | |
| 145 | + | echo " <title>$TITLE</title>" | |
| 146 | + | echo " <link>$LINK</link>" | |
| 147 | + | echo " <lastBuildDate>$NOW</lastBuildDate>" | |
| 148 | + | echo " <description>Podcast de ${DIR_NAME} generat automaticament</description>" | |
| 149 | + | echo " <image>" | |
| 150 | + | echo " <url>$IMAGE_URL</url>" | |
| 151 | + | echo " <title>$TITLE</title>" | |
| 152 | + | echo " <link>$LINK</link>" | |
| 153 | + | echo " </image>" | |
| 154 | + | echo " <itunes:image href='$IMAGE_URL' />" | |
| 155 | + | ||
| 156 | + | find "$path" -type f \( -name "*.mp3" -o -name "*.m4a" \) | sort -r | while read -r audio; do | |
| 157 | + | FILENAME=$(basename "$audio") | |
| 158 | + | RELATIVE_PATH=${audio#$BASE_DIR/} | |
| 159 | + | URL_ENCODED=$(echo "${LINK_BASE}/${RELATIVE_PATH}" | sed 's/ /%20/g; s|\([^:]\)//|\1/|g') | |
| 160 | + | PUBDATE=$(date -R -r "$audio") | |
| 161 | + | echo " <item>" | |
| 162 | + | echo " <title>${FILENAME%.*}</title>" | |
| 163 | + | echo " <itunes:author>vctrsnts</itunes:author>" | |
| 164 | + | echo " <link>$URL_ENCODED</link>" | |
| 165 | + | echo " <guid isPermaLink=\"false\">${FILENAME}</guid>" | |
| 166 | + | echo " <enclosure url=\"$URL_ENCODED\" type=\"audio/mpeg\" />" | |
| 167 | + | echo " <pubDate>$PUBDATE</pubDate>" | |
| 168 | + | echo " <description>${FILENAME%.*}</description>" | |
| 169 | + | echo " </item>" | |
| 170 | + | done | |
| 171 | + | ||
| 172 | + | echo "</channel>" | |
| 173 | + | echo "</rss>" | |
| 174 | + | } > "$OUTPUT_FILE" | |
| 175 | + | ||
| 176 | + | chown root:root "$OUTPUT_FILE" | |
| 177 | + | ||
| 178 | + | # --- Notificación única ---------------------------------------------------------- | |
| 179 | + | # Envia un unic missatge NTFY amb el resum complet de l'operacio: | |
| 180 | + | # episodi processat + feed regenerat. | |
| 181 | + | # S'usa text/plain per evitar problemes de format amb el body de curl. | |
| 182 | + | #NTFY_BODY="<200b>Nou episodi: | |
| 183 | + | #- ${titol} (${rss_pubdate}) | |
| 184 | + | # | |
| 185 | + | #Feed actualitzat: | |
| 186 | + | #- ${DIR_NAME}.xml" | |
| 187 | + | # | |
| 188 | + | NTFY_BODY=" | |
| 189 | + | Nou episodi: | |
| 190 | + | - ${titol} (${rss_pubdate}) | |
| 191 | + | ||
| 192 | + | Feed actualitzat: | |
| 193 | + | - ${DIR_NAME}.xml" | |
| 194 | + | ||
| 195 | + | curl -s \ | |
| 196 | + | -o /dev/null \ | |
| 197 | + | 2>/dev/null \ | |
| 198 | + | -H "Authorization: Bearer ${NTFY_TOKEN}" \ | |
| 199 | + | -H "Title: Podcast Sync" \ | |
| 200 | + | -H "Tags: headphones" \ | |
| 201 | + | -H "Priority: default" \ | |
| 202 | + | -H "Content-Type: text/plain" \ | |
| 203 | + | -d "${NTFY_BODY}" \ | |
| 204 | + | "${NTFY_SERVER}/${NTFY_TOPIC}" | |
vsantos / ingestEpisode
Last active 1 day ago
Script para la gestión de ficheros de mp3 y m4a