From da6dabcc622adbcfe1a220f02b7ef3d737b413c2 Mon Sep 17 00:00:00 2001 From: Guillem Hernandez Sola Date: Tue, 7 Apr 2026 19:37:59 +0200 Subject: [PATCH] Added all --- .gitignore | 177 ++++++++++++++++++ README.md | 1 + file-management/buscar_grandes.sh | 9 + file-management/filtrar_excel_per_csv.py | 57 ++++++ file-management/juntar_csv.py | 67 +++++++ file-management/mautic.py | 73 ++++++++ file-management/mautic_tags.py | 84 +++++++++ file-management/nan-mautic.py | 17 ++ file-management/netejar_desuscrits.py | 53 ++++++ file-management/netejar_excel.py | 32 ++++ file-management/split_excel.py | 46 +++++ file-management/teams.py | 104 ++++++++++ file-management/treure_etiquetes_mautic.py | 40 ++++ file-management/unificar_excels.py | 19 ++ file-management/unir_excels_pestanyes.py | 40 ++++ linkedin/linkedin-posts.py | 81 ++++++++ media/audio-extractor.sh | 36 ++++ media/convert_mp4_to_webm.sh | 45 +++++ media/copy_cbr_files.sh | 12 ++ media/download_instagram_images.sh | 48 +++++ media/export_instagram_cookies.py | 22 +++ media/image-instagram-downloader.sh | 57 ++++++ media/instagram-downloader.sh | 33 ++++ media/spotify-rss.py | 61 ++++++ media/url-video-downloader.sh | 33 ++++ media/video-downloader.sh | 34 ++++ media/webm-to-mp4-converter.sh | 44 +++++ media/zip_cbr_files.sh | 13 ++ odilo/biblio_odilo.py | 31 +++ odilo/recollir_epubs.sh | 21 +++ outlook/email_login.py | 43 +++++ outlook/get_token.py | 177 ++++++++++++++++++ outlook/marti.py | 142 ++++++++++++++ pdfs/split_pdf_pages.sh | 33 ++++ replace_remove/fix-all-special-characters.sh | 7 + .../remove_parentheses_files_folders.sh | 23 +++ replace_remove/remove_parentheses_folders.sh | 23 +++ .../replace_hyphen_with_underscores.sh | 23 +++ .../replace_spaces_with_underscores.sh | 23 +++ .../replace_triple_with_underscores.sh | 23 +++ ...ace_under_hyphen_under_with_underscores.sh | 23 +++ wordpress/export-articles.py | 29 +++ 42 files changed, 1959 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 file-management/buscar_grandes.sh create mode 100644 file-management/filtrar_excel_per_csv.py create mode 100644 file-management/juntar_csv.py create mode 100644 file-management/mautic.py create mode 100644 file-management/mautic_tags.py create mode 100644 file-management/nan-mautic.py create mode 100644 file-management/netejar_desuscrits.py create mode 100644 file-management/netejar_excel.py create mode 100644 file-management/split_excel.py create mode 100644 file-management/teams.py create mode 100644 file-management/treure_etiquetes_mautic.py create mode 100644 file-management/unificar_excels.py create mode 100644 file-management/unir_excels_pestanyes.py create mode 100644 linkedin/linkedin-posts.py create mode 100755 media/audio-extractor.sh create mode 100755 media/convert_mp4_to_webm.sh create mode 100644 media/copy_cbr_files.sh create mode 100755 media/download_instagram_images.sh create mode 100644 media/export_instagram_cookies.py create mode 100755 media/image-instagram-downloader.sh create mode 100755 media/instagram-downloader.sh create mode 100644 media/spotify-rss.py create mode 100755 media/url-video-downloader.sh create mode 100755 media/video-downloader.sh create mode 100755 media/webm-to-mp4-converter.sh create mode 100755 media/zip_cbr_files.sh create mode 100644 odilo/biblio_odilo.py create mode 100644 odilo/recollir_epubs.sh create mode 100644 outlook/email_login.py create mode 100644 outlook/get_token.py create mode 100644 outlook/marti.py create mode 100644 pdfs/split_pdf_pages.sh create mode 100755 replace_remove/fix-all-special-characters.sh create mode 100755 replace_remove/remove_parentheses_files_folders.sh create mode 100755 replace_remove/remove_parentheses_folders.sh create mode 100755 replace_remove/replace_hyphen_with_underscores.sh create mode 100755 replace_remove/replace_spaces_with_underscores.sh create mode 100755 replace_remove/replace_triple_with_underscores.sh create mode 100755 replace_remove/replace_under_hyphen_under_with_underscores.sh create mode 100644 wordpress/export-articles.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c52daf1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,177 @@ +# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,zsh +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,zsh +media/downloads +media/xhs_videos +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.DS_Store +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +*.txt +*.csv +*.xlsx +*.xls +.env +*.env + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Zsh ### +# Zsh compiled script + zrecompile backup +*.zwc +*.zwc.old + +# Zsh completion-optimization dumpfile +*zcompdump* + +# Zsh history +.zsh_history + +# Zsh sessions +.zsh_sessions + +# Zsh zcalc history +.zcalc_history + +# A popular plugin manager's files +._zinit +.zinit_lstupd + +# zdharma/zshelldoc tool's files +zsdoc/data + +# robbyrussell/oh-my-zsh/plugins/per-directory-history plugin's files +# (when set-up to store the history in the local directory) +.directory_history + +# MichaelAquilina/zsh-autoswitch-virtualenv plugin's files +# (for Zsh plugins using Python) +.venv + +# Zunit tests' output +/tests/_output/* +!/tests/_output/.gitkeep + +# End of https://www.toptal.com/developers/gitignore/api/intellij+all,zsh + +downloads/ + +# Created by https://www.toptal.com/developers/gitignore/api/macos +# Edit at https://www.toptal.com/developers/gitignore?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud +*.csv + +# End of https://www.toptal.com/developers/gitignore/api/macos diff --git a/README.md b/README.md new file mode 100644 index 0000000..458b3cc --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# scripts diff --git a/file-management/buscar_grandes.sh b/file-management/buscar_grandes.sh new file mode 100644 index 0000000..0bfbe83 --- /dev/null +++ b/file-management/buscar_grandes.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Directorio base desde donde buscar (puedes cambiarlo o usar el argumento del script) +DIRECTORIO=${1:-.} + +# Encuentra archivos mayores a 1GB y muestra su tamaño ordenado +echo "Archivos mayores a 1GB en el directorio: $DIRECTORIO" +find "$DIRECTORIO" -type f -size +1G -exec du -h {} + | sort -hr +echo "Búsqueda completada." \ No newline at end of file diff --git a/file-management/filtrar_excel_per_csv.py b/file-management/filtrar_excel_per_csv.py new file mode 100644 index 0000000..676f15d --- /dev/null +++ b/file-management/filtrar_excel_per_csv.py @@ -0,0 +1,57 @@ +import pandas as pd +import os +import sys + +print("🔍 --- Filtrant Excel segons usuaris actius del CSV ---") + +# Noms dels fitxers +fitxer_csv = "Newsletter_Combinat_Final_Actius.csv" +fitxer_excel = "Contactos_Unificats.xlsx" +fitxer_resultat = "Contactos_Unificats_Filtrats.xlsx" + +# Comprovem que existeixen +if not os.path.exists(fitxer_csv) or not os.path.exists(fitxer_excel): + print("❌ Error: Assegura't que els fitxers CSV i Excel són a la mateixa carpeta.") + sys.exit(1) + +try: + print("⏳ Llegint els usuaris actius del CSV...") + # Llegim el CSV (sabem que està separat per punt i coma) + df_csv = pd.read_csv(fitxer_csv, sep=';', encoding='utf-8') + + # Extraiem els correus de la columna 'Email_ID', els passem a minúscules i traiem espais en blanc + # per assegurar-nos que coincideixen perfectament + correus_actius = set(df_csv['Email_ID'].dropna().astype(str).str.lower().str.strip()) + print(f"✅ S'han carregat {len(correus_actius)} correus únics del CSV.") + + print("⏳ Processant l'Excel pestanya per pestanya...") + # Obrim l'Excel original i preparem el nou per escriure + excel_original = pd.ExcelFile(fitxer_excel) + + with pd.ExcelWriter(fitxer_resultat, engine='openpyxl') as writer: + for nom_pestanya in excel_original.sheet_names: + # Llegim la pestanya actual + df_pestanya = pd.read_excel(excel_original, sheet_name=nom_pestanya) + total_inicial = len(df_pestanya) + + # Normalitzem la columna 'email' de l'Excel per poder comparar-la + if 'email' in df_pestanya.columns: + correus_excel = df_pestanya['email'].astype(str).str.lower().str.strip() + + # Filtrem: ens quedem només amb les files on el correu estigui a la nostra llista d'actius + df_filtrat = df_pestanya[correus_excel.isin(correus_actius)] + total_final = len(df_filtrat) + + print(f" 👉 Pestanya '{nom_pestanya}': s'han mantingut {total_final} de {total_inicial} contactes.") + else: + print(f" ⚠️ Avís: La pestanya '{nom_pestanya}' no té cap columna anomenada 'email'. Es deixa intacta.") + df_filtrat = df_pestanya + + # Guardem el resultat a la mateixa pestanya del nou fitxer + df_filtrat.to_excel(writer, sheet_name=nom_pestanya, index=False) + + print(f"\n🎉 Procés completat amb èxit!") + print(f"📄 S'ha creat el fitxer amb els contactes filtrats: {fitxer_resultat}") + +except Exception as e: + print(f"❌ S'ha produït un error: {e}") diff --git a/file-management/juntar_csv.py b/file-management/juntar_csv.py new file mode 100644 index 0000000..49567e0 --- /dev/null +++ b/file-management/juntar_csv.py @@ -0,0 +1,67 @@ +import csv +import glob +import os +import sys + +# 1. Comprovem si s'ha passat la ruta com a argument +if len(sys.argv) < 2: + print("❌ Error: Has d'indicar la ruta de la carpeta.") + print("💡 Ús correcte: python juntar_csv.py /ruta/de/la/carpeta") + sys.exit(1) + +# 2. Agafem la ruta (el primer argument després del nom de l'script) +carpeta_origen = sys.argv[1] + +# Netegem possibles cometes que la terminal hagi afegit +carpeta_origen = carpeta_origen.strip("'").strip('"').strip() + +# 3. Comprovem que la carpeta existeix +if not os.path.isdir(carpeta_origen): + print(f"❌ Error: La ruta '{carpeta_origen}' no és vàlida o no és una carpeta.") + sys.exit(1) + +print(f"📁 Cercant fitxers CSV a: {carpeta_origen}") + +ruta_cerca = os.path.join(carpeta_origen, "*.csv") +fitxers_csv = glob.glob(ruta_cerca) + +# Definim el nom del fitxer final +fitxer_resultat = os.path.join(carpeta_origen, "Newsletter_Combinat_Final.csv") + +if not fitxers_csv: + print("⚠️ No s'han trobat fitxers CSV a la carpeta indicada.") +else: + # Filtrem per no incloure el fitxer resultat si ja existeix d'una execució anterior + fitxers_a_processar = [f for f in fitxers_csv if f != fitxer_resultat] + + if not fitxers_a_processar: + print("⚠️ Només s'ha trobat el fitxer combinat anterior. No hi ha res de nou per unir.") + sys.exit(0) + + print(f"🔄 S'han trobat {len(fitxers_a_processar)} fitxers. Processant...") + + # 4. Unifiquem els fitxers + with open(fitxer_resultat, 'w', newline='', encoding='utf-8') as sortida: + escrivent = csv.writer(sortida, delimiter=';') + + fitxers_processats = 0 + for nom_fitxer in fitxers_a_processar: + with open(nom_fitxer, 'r', encoding='utf-8') as f: + lector = csv.reader(f, delimiter=';') + try: + capcalera = next(lector) + except StopIteration: + continue # Saltem el fitxer si està completament buit + + # Escrivim la capçalera només pel primer fitxer vàlid + if fitxers_processats == 0: + escrivent.writerow(capcalera) + + # Escrivim la resta de dades + for fila in lector: + escrivent.writerow(fila) + + fitxers_processats += 1 + + print(f"✅ Èxit! S'han combinat {fitxers_processats} fitxers.") + print(f"📄 Fitxer creat a: {fitxer_resultat}") \ No newline at end of file diff --git a/file-management/mautic.py b/file-management/mautic.py new file mode 100644 index 0000000..25a5f66 --- /dev/null +++ b/file-management/mautic.py @@ -0,0 +1,73 @@ +import pandas as pd +import csv + +def filtrar_i_consolidar_etiquetes(tags_str): + """Filtra i agrupa les etiquetes segons les regles de negoci.""" + if pd.isna(tags_str) or str(tags_str).strip().lower() == 'nan': + return '' + + # 1. Separem les etiquetes i les netegem + llista_tags = [t.strip().lower() for t in str(tags_str).split(',')] + + # Utilitzem un 'set' per evitar etiquetes duplicades al final + tags_finals = set() + + for t in llista_tags: + # 2. Regla de conservació directa: test_, int_ i rrhh + if t.startswith('test_') or t.startswith('int_') or t == 'rrhh': + tags_finals.add(t) + continue + + # 3. Regles d'agrupació (l'ordre és important per als prefixos llargs) + if t.startswith('skupspo2-') or t.startswith('pspo2-'): + tags_finals.add('pspo2') + elif t.startswith('pspo-'): + tags_finals.add('pspo') + elif t.startswith('psm2-'): + tags_finals.add('psm2') + elif t.startswith('psm-'): + tags_finals.add('psm') + elif t.startswith('psux-') or t.startswith('psu-'): + tags_finals.add('psu') + elif t.startswith('sps-'): + tags_finals.add('sps') + elif t.startswith('safe-'): + tags_finals.add('safe-ls') + elif t.startswith('pal_ebm-'): + tags_finals.add('pal_ebm') + elif t.startswith('pal-'): + tags_finals.add('pal') + + # 4. Si tenim etiquetes vàlides, les ordenem, les unim amb | i hi posem cometes + if tags_finals: + # Ordenar-les (sorted) fa que el resultat sigui més fàcil de llegir al CSV + tags_units = '|'.join(sorted(list(tags_finals))) + return f'"{tags_units}"' + else: + return '' + +# --- INICI DEL PROCÉS --- + +# 1. Carregar el fitxer original +df = pd.read_excel('contactes_mautic/Contactos_2602026_7881pax.xlsx') + +# 2. Seleccionar només les columnes que ens interessen +columnes_mautic = ['email', 'nombre_x', 'apellidos_x', '*ciudad_x', '*pais_x', 'etiquetas_x'] +df_net = df[columnes_mautic].copy() + +# 3. Reanomenar les columnes perquè Mautic les auto-detecti +df_net.rename(columns={ + 'nombre_x': 'firstname', + 'apellidos_x': 'lastname', + '*ciudad_x': 'city', + '*pais_x': 'country', + 'etiquetas_x': 'etiquetes' +}, inplace=True) + +# 4. APLICAR EL FILTRE I LA CONSOLIDACIÓ +df_net['etiquetes'] = df_net['etiquetes'].apply(filtrar_i_consolidar_etiquetes) + +# 5. Guardar com a CSV llest per importar +df_net.to_csv('Contactes_Mautic_Consolidats.csv', index=False, encoding='utf-8', quoting=csv.QUOTE_NONE, escapechar='\\') + +print("✅ Fitxer preparat! Les etiquetes s'han filtrat i agrupat correctament.") \ No newline at end of file diff --git a/file-management/mautic_tags.py b/file-management/mautic_tags.py new file mode 100644 index 0000000..aa864f7 --- /dev/null +++ b/file-management/mautic_tags.py @@ -0,0 +1,84 @@ +import pandas as pd +import os +import csv + +def simplificar_etiqueta(tag): + """Filtra i redueix les etiquetes a la seva essència.""" + tag = tag.strip().lower() + + # 1. Traducció dels noms llargs oficials + if 'professional-scrum-master' in tag: return 'psm' + if 'professional-scrum-product-owner' in tag: return 'pspo' + if 'professional-scrum-developer' in tag: return 'psd' + if 'professional-agile-leadership' in tag: return 'pal' + + # 2. Protecció de nivells avançats (Molt important posar-ho abans que els normals) + if 'psm2' in tag or 'psm-2' in tag: return 'psm2' + if 'pspo2' in tag or 'pspo-2' in tag: return 'pspo2' + if 'pspo-a' in tag: return 'pspo-a' + + # 3. Agrupació per paraula clau (Si conté la paraula, s'assigna a l'etiqueta base) + if 'psm' in tag: return 'psm' + if 'pspo' in tag: return 'pspo' + if 'psu' in tag: return 'psu' + if 'psd' in tag: return 'psd' + if 'aps' in tag: return 'aps' + if 'apk' in tag: return 'apk' + if 'pal' in tag: return 'pal' + if 'sps' in tag: return 'sps' + + # 4. Altres agrupacions útils que he detectat a la teva llista + if 'okr' in tag: return 'okr' + if 'scrumday' in tag or 'sd20' in tag or 'sd21' in tag: return 'scrumday' + + # 5. Si no és cap de les importants, la deixem tal qual (però neta de test_) + if tag.startswith('test_'): + return tag.replace('test_', 'int_', 1) + + return tag + +# --- INICI DEL PROCÉS --- + +nom_fitxer = 'contactes_mautic/Contactos_2602026_7881pax.xlsx' +print(f"Llegint el fitxer {nom_fitxer}...") +df = pd.read_excel(nom_fitxer) + +columnes = { + 'email': 'email', + 'nombre_x': 'firstname', + 'apellidos_x': 'lastname', + '*ciudad_x': 'city', + '*pais_x': 'country', + 'etiquetas_x': 'tags' +} +df_net = df[list(columnes.keys())].rename(columns=columnes) +df_net = df_net.dropna(subset=['email', 'tags']) + +carpeta_sortida = 'Mautic_CSVs_per_Tag' +os.makedirs(carpeta_sortida, exist_ok=True) + +# Apliquem la nostra súper funció de simplificació +df_net['tag_individual'] = df_net['tags'].apply( + lambda x: [simplificar_etiqueta(tag) for tag in str(x).split(',') if tag.strip()] +) + +# Expandim i eliminem duplicats +df_exploded = df_net.explode('tag_individual') +df_exploded = df_exploded.drop_duplicates(subset=['email', 'tag_individual']) + +etiquetes_uniques = df_exploded['tag_individual'].unique() +print(f"🎉 Màgia feta! Hem reduït centenars d'etiquetes a només {len(etiquetes_uniques)} úniques.") + +for tag in etiquetes_uniques: + df_tag = df_exploded[df_exploded['tag_individual'] == tag].copy() + + df_tag['tags'] = tag + df_tag = df_tag.rename(columns={'tags': tag}) + df_tag = df_tag.drop(columns=['tag_individual']) + + nom_tag_net = str(tag).replace(' ', '_').replace('/', '_').replace(':', '') + ruta_fitxer = os.path.join(carpeta_sortida, f"etiqueta_{nom_tag_net}.csv") + + df_tag.to_csv(ruta_fitxer, index=False, encoding='utf-8', sep=',', quoting=csv.QUOTE_ALL) + +print(f"✅ Tots els fitxers nets estan a la carpeta '{carpeta_sortida}'.") \ No newline at end of file diff --git a/file-management/nan-mautic.py b/file-management/nan-mautic.py new file mode 100644 index 0000000..e82b160 --- /dev/null +++ b/file-management/nan-mautic.py @@ -0,0 +1,17 @@ +import pandas as pd + +# 1. Carregar el fitxer original +df = pd.read_excel('contactes_mautic/Contactos_2602026_7881pax.xlsx') + +# 2. Quedar-nos només amb els emails vàlids +df_net = df.dropna(subset=['email'])[['email']].copy() + +# 3. Ordre d'esborrat múltiple +# Afegim el signe '-' davant de l'etiqueta estranya i també del 'nan' normal +# La 'r' al davant indica a Python que llegeixi les barres invertides literalment +df_net['etiquetes'] = r'-\"-nan\"|-nan' + +# 4. Guardem el CSV de manera natural (sense forçar paràmetres d'escapament) +df_net.to_csv('Neteja_Definitiva_Tags.csv', index=False, encoding='utf-8') + +print("✅ Fitxer preparat! Les etiquetes errònies s'han marcat per ser esborrades.") \ No newline at end of file diff --git a/file-management/netejar_desuscrits.py b/file-management/netejar_desuscrits.py new file mode 100644 index 0000000..44589fb --- /dev/null +++ b/file-management/netejar_desuscrits.py @@ -0,0 +1,53 @@ +import csv +import sys +import os + +# Comprovem que s'ha passat el fitxer com a argument +if len(sys.argv) < 2: + print("❌ Error: Has d'indicar la ruta del fitxer CSV.") + print("💡 Ús correcte: python netejar_desuscrits.py /ruta/al/Newsletter_Combinat_Final.csv") + sys.exit(1) + +fitxer_origen = sys.argv[1].strip("'").strip('"').strip() + +if not os.path.isfile(fitxer_origen): + print(f"❌ Error: No s'ha trobat el fitxer '{fitxer_origen}'.") + sys.exit(1) + +fitxer_desti = fitxer_origen.replace(".csv", "_Actius.csv") + +usuaris_eliminats = 0 +usuaris_actius = 0 + +print(f"🔍 Analitzant el fitxer: {fitxer_origen}") + +with open(fitxer_origen, 'r', encoding='utf-8') as f_in, \ + open(fitxer_desti, 'w', newline='', encoding='utf-8') as f_out: + + lector = csv.reader(f_in, delimiter=';') + escrivent = csv.writer(f_out, delimiter=';') + + # Llegim i escrivim la capçalera + capcalera = next(lector) + escrivent.writerow(capcalera) + + # Busquem l'índex de la columna de desuscripció + try: + index_unsub = capcalera.index("Unsubscribe_Date") + except ValueError: + print("❌ Error: No s'ha trobat la columna 'Unsubscribe_Date' al fitxer.") + sys.exit(1) + + # Filtrem les files + for fila in lector: + # Si la columna està buida, l'usuari és actiu + if len(fila) > index_unsub and not fila[index_unsub].strip(): + escrivent.writerow(fila) + usuaris_actius += 1 + else: + usuaris_eliminats += 1 + +print("✅ Neteja completada amb èxit!") +print(f"📉 Usuaris desuscrits eliminats: {usuaris_eliminats}") +print(f"📈 Usuaris actius conservats: {usuaris_actius}") +print(f"📄 Nou fitxer creat a: {fitxer_desti}") diff --git a/file-management/netejar_excel.py b/file-management/netejar_excel.py new file mode 100644 index 0000000..87644db --- /dev/null +++ b/file-management/netejar_excel.py @@ -0,0 +1,32 @@ +import pandas as pd + +# 1. Definir los nombres de los archivos +archivo_entrada = 'Contactos_2602026_totales.xlsx' +archivo_salida = 'Contactos_2602026_Limpios.xlsx' + +print(f"Cargando el archivo: {archivo_entrada}...") + +# 2. Leer el archivo Excel +# Asumimos que los datos están en la primera hoja (Sheet1) +df = pd.read_excel(archivo_entrada) + +# Contar cuántos registros hay inicialmente +total_inicial = len(df) +print(f"Registros iniciales encontrados: {total_inicial}") + +# 3. Eliminar duplicados basados en la columna 'email' +# keep='first' conserva la primera aparición del correo y elimina las siguientes +df_limpio = df.drop_duplicates(subset=['email'], keep='first') + +# Contar cuántos registros quedaron y cuántos se eliminaron +total_final = len(df_limpio) +duplicados_eliminados = total_inicial - total_final + +print(f"Se han eliminado {duplicados_eliminados} contactos duplicados.") +print(f"Registros finales únicos: {total_final}") + +# 4. Exportar el resultado a un nuevo archivo Excel +print("Guardando el nuevo archivo limpio...") +df_limpio.to_excel(archivo_salida, index=False) + +print(f"¡Proceso completado! Archivo guardado como: {archivo_salida}") diff --git a/file-management/split_excel.py b/file-management/split_excel.py new file mode 100644 index 0000000..912a953 --- /dev/null +++ b/file-management/split_excel.py @@ -0,0 +1,46 @@ +import pandas as pd +import numpy as np +import math + +# Load the Excel file +file_name = 'Contactos_2602026_7881pax.xlsx' + +print("Llegint el fitxer Excel...") + +# --- CORRECCIÓ AQUÍ --- +# Fem servir read_excel en lloc de read_csv +df = pd.read_excel(file_name) +# ---------------------- + +# Display head and info to understand the structure +print(df.head()) +print(df.info()) + +# Total number of rows +total_rows = len(df) +print(f"Total rows: {total_rows}") + +# Number of splits +n = 6 +# Fem servir math.ceil o np.ceil per assegurar que no deixem files fora +chunk_size = int(np.ceil(total_rows / n)) + +# Split and save +output_files = [] +print(f"Dividint en {n} parts...") + +for i in range(n): + start_row = i * chunk_size + end_row = min((i + 1) * chunk_size, total_rows) + + # Si per algun motiu start_row supera el total, parem + if start_row >= total_rows: + break + + chunk = df.iloc[start_row:end_row] + + output_filename = f'Contactos_linkedin_part_{i+1}.xlsx' + chunk.to_excel(output_filename, index=False) + output_files.append(output_filename) + +print(f"Files created: {output_files}") \ No newline at end of file diff --git a/file-management/teams.py b/file-management/teams.py new file mode 100644 index 0000000..0fd1351 --- /dev/null +++ b/file-management/teams.py @@ -0,0 +1,104 @@ +import pandas as pd +import glob +import re +import os +import csv + +def extreure_minuts(temps_str): + """Converteix cadenes de text com '2h 15m', '45m' o '1h' a minuts totals.""" + if pd.isna(temps_str): + return 0 + temps_str = str(temps_str).lower() + hores = 0 + minuts = 0 + + match_h = re.search(r'(\d+)\s*h', temps_str) + if match_h: + hores = int(match_h.group(1)) + + match_m = re.search(r'(\d+)\s*m', temps_str) + if match_m: + minuts = int(match_m.group(1)) + + return (hores * 60) + minuts + +fitxers = glob.glob("*.csv") +dades_alumnes = {} + +if not fitxers: + print("⚠️ No s'han trobat fitxers CSV a la carpeta actual.") +else: + print(f"S'han trobat {len(fitxers)} fitxers. Processant dades...\n") + +for fitxer in fitxers: + try: + # 1. Obrim el fitxer manualment per buscar a quina fila comencen les dades + with open(fitxer, 'r', encoding='utf-16') as f: + linies = f.readlines() + + fila_capcalera = -1 + separador = '\t' + + # Busquem la línia que conté "Nom" o "Nombre" o "Name" + for i, linia in enumerate(linies): + if 'Nom' in linia or 'Nombre' in linia or 'Name' in linia: + fila_capcalera = i + # Detectem si fa servir tabulacions o comes + if ',' in linia and '\t' not in linia: + separador = ',' + break + + if fila_capcalera == -1: + print(f"⚠️ Saltant '{fitxer}': No s'ha trobat cap fila amb la paraula 'Nom'.") + continue + + # 2. Llegim el CSV dient-li exactament on comença + df = pd.read_csv(fitxer, sep=separador, encoding='utf-16', skiprows=fila_capcalera) + + # 3. Busquem les columnes dinàmicament + col_durada = next((col for col in df.columns if 'durada' in col.lower() or 'duración' in col.lower() or 'duration' in col.lower()), None) + col_nom = next((col for col in df.columns if 'nom' in col.lower() or 'nombre' in col.lower() or 'name' in col.lower()), None) + + if not col_nom or not col_durada: + print(f"⚠️ Saltant '{fitxer}': Columnes invàlides. Trobades: {list(df.columns)}") + continue + + for index, row in df.iterrows(): + nom = row[col_nom] + if pd.isna(nom): + continue + + minuts = extreure_minuts(row[col_durada]) + + # Apliquem la regla del 5 de febrer (límit de 205 minuts) + if "2-05-26" in fitxer and minuts > 205: + minuts = 205 + + if nom in dades_alumnes: + dades_alumnes[nom] += minuts + else: + dades_alumnes[nom] = minuts + + except UnicodeError: + print(f"❌ Error de codificació al fitxer '{fitxer}'. Intenta obrir-lo i guardar-lo de nou.") + except Exception as e: + print(f"❌ Error processant el fitxer '{fitxer}': {e}") + +# 4. Resultats +if dades_alumnes: + print("="*50) + print("📊 RESULTATS D'ASSISTÈNCIA (Mínim requerit: 1581 min)") + print("="*50) + + for nom in sorted(dades_alumnes.keys()): + minuts_totals = dades_alumnes[nom] + + if minuts_totals >= 1581: + estat = "✅ Supera el 80%" + else: + estat = "❌ No arriba" + + h = minuts_totals // 60 + m = minuts_totals % 60 + + print(f"{nom}: {minuts_totals} minuts ({h}h {m}m) -> {estat}") \ No newline at end of file diff --git a/file-management/treure_etiquetes_mautic.py b/file-management/treure_etiquetes_mautic.py new file mode 100644 index 0000000..e3d06f2 --- /dev/null +++ b/file-management/treure_etiquetes_mautic.py @@ -0,0 +1,40 @@ +import pandas as pd +import csv + +def preparar_etiquetes_per_esborrar(tags_str): + """Afegeix un '-' a cada etiqueta, les uneix amb | i les tanca entre cometes.""" + # 1. Si la cel·la està buida, la deixem en blanc + if pd.isna(tags_str) or str(tags_str).strip().lower() == 'nan': + return '' + + # 2. Separem les etiquetes originals per coma i traiem espais + llista_tags = [t.strip() for t in str(tags_str).split(',')] + + # 3. Afegim el signe '-' davant de CADA etiqueta (excepte si està buida) + tags_per_esborrar = [f"-{t}" for t in llista_tags if t] + + # 4. Les unim amb | i hi afegim les cometes dobles manualment + if tags_per_esborrar: + tags_units = '|'.join(tags_per_esborrar) + return f'"{tags_units}"' + else: + return '' + +# --- INICI DEL PROCÉS --- + +# 1. Carregar el fitxer original +df = pd.read_excel('contactes_mautic/Contactos_2602026_7881pax.xlsx') + +# 2. Seleccionar només l'email i les etiquetes +df_esborrar = df[['email', 'etiquetas_x']].copy() + +# 3. Reanomenar la columna +df_esborrar.rename(columns={'etiquetas_x': 'etiquetes'}, inplace=True) + +# 4. APLICAR LA TRANSFORMACIÓ +df_esborrar['etiquetes'] = df_esborrar['etiquetes'].apply(preparar_etiquetes_per_esborrar) + +# 5. Guardar el CSV respectant les nostres cometes literals +df_esborrar.to_csv('Contactes_Mautic_Esborrar_Etiquetes.csv', index=False, encoding='utf-8', quoting=csv.QUOTE_NONE, escapechar='\\') + +print("✅ Fitxer preparat! Les etiquetes tenen el '-' i estan entre cometes dobles.") \ No newline at end of file diff --git a/file-management/unificar_excels.py b/file-management/unificar_excels.py new file mode 100644 index 0000000..bd6381a --- /dev/null +++ b/file-management/unificar_excels.py @@ -0,0 +1,19 @@ +import pandas as pd + +# 1. Cargar el primer archivo (tiene encabezados) +df1 = pd.read_excel('Contactos_linkedin_120126_agile_6589pax_filtrado.xlsx') + +# 2. Cargar el segundo archivo (sin encabezados, asignamos los principales) +# Asumimos que las primeras 3 columnas son email, apellidos y nombre +df2 = pd.read_excel('lista_contactos_brevo_20251023_1394pax_AGILE611.xlsx', header=None) +df2.rename(columns={0: 'email', 1: 'apellidos_x', 2: 'nombre_x'}, inplace=True) + +# 3. Unir ambos archivos (apilarlos uno debajo del otro) +df_final = pd.concat([df1, df2], ignore_index=True) + +# 4. Eliminar contactos duplicados basados en la columna 'email' +df_final.drop_duplicates(subset=['email'], keep='first', inplace=True) + +# 5. Exportar el resultado a un nuevo archivo Excel +df_final.to_excel('Contactos_Unificados_Agile611.xlsx', index=False) +print("¡Archivo unificado creado con éxito!") diff --git a/file-management/unir_excels_pestanyes.py b/file-management/unir_excels_pestanyes.py new file mode 100644 index 0000000..c646f40 --- /dev/null +++ b/file-management/unir_excels_pestanyes.py @@ -0,0 +1,40 @@ +import pandas as pd +import os +import sys + +print("📊 --- Unificador de fitxers Excel ---") + +# Definim els noms dels fitxers d'entrada i el de sortida +fitxer1 = "Contactos_rrhh_040226.xlsx" +fitxer2 = "Contactos_2602026_7881pax.xlsx" +fitxer_resultat = "Contactos_Unificats.xlsx" + +# Comprovem que els fitxers existeixen a la carpeta actual +if not os.path.exists(fitxer1): + print(f"❌ Error: No s'ha trobat el fitxer '{fitxer1}'.") + sys.exit(1) + +if not os.path.exists(fitxer2): + print(f"❌ Error: No s'ha trobat el fitxer '{fitxer2}'.") + sys.exit(1) + +print("⏳ Llegint els fitxers... (això pot trigar uns segons depenent de la mida)") + +try: + # Llegim els dos fitxers Excel + df1 = pd.read_excel(fitxer1) + df2 = pd.read_excel(fitxer2) + + # Creem el nou fitxer Excel amb múltiples pestanyes + with pd.ExcelWriter(fitxer_resultat, engine='openpyxl') as writer: + # Escrivim cada DataFrame en una pestanya diferent + df1.to_excel(writer, sheet_name='RRHH', index=False) + df2.to_excel(writer, sheet_name='Contactos_7881', index=False) + + print(f"✅ Procés completat amb èxit!") + print(f"📄 S'ha creat el fitxer: {fitxer_resultat}") + print(" - Pestanya 1: 'RRHH'") + print(" - Pestanya 2: 'Contactos_7881'") + +except Exception as e: + print(f"❌ S'ha produït un error durant el procés: {e}") diff --git a/linkedin/linkedin-posts.py b/linkedin/linkedin-posts.py new file mode 100644 index 0000000..02fd5c1 --- /dev/null +++ b/linkedin/linkedin-posts.py @@ -0,0 +1,81 @@ +import os +import requests +from dotenv import load_dotenv + +# 1. Load the environment variables +load_dotenv() + +ACCESS_TOKEN = os.getenv('LINKEDIN_ACCESS_TOKEN') +ORGANIZATION_ID = os.getenv('LINKEDIN_ORG_ID') + +# LinkedIn requires a version header (Format: YYYYMM) +API_VERSION = '202602' + +def get_all_linkedin_posts(access_token, org_id): + """ + Fetches ALL posts from a specific LinkedIn Organization Page using pagination. + """ + if not access_token or not org_id: + print("🚨 Error: Missing credentials. Please check your .env file.") + return [] + + url = "https://api.linkedin.com/rest/posts" + + headers = { + "Authorization": f"Bearer {access_token}", + "LinkedIn-Version": API_VERSION, + "X-Restli-Protocol-Version": "2.0.0" + } + + all_posts = [] + start = 0 + count = 100 # Maximum allowed by LinkedIn per request + + print(f"📥 Starting to fetch posts for Organization ID: {org_id}...") + + while True: + params = { + "q": "author", + "author": f"urn:li:organization:{org_id}", + "count": count, + "start": start + } + + try: + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + + data = response.json() + elements = data.get('elements', []) + + # If no more posts are returned, we've reached the end! + if not elements: + break + + all_posts.extend(elements) + print(f"✅ Fetched posts {start + 1} to {start + len(elements)}...") + + # Increment the starting point for the next page + start += count + + except requests.exceptions.RequestException as e: + print(f"❌ Error fetching posts at offset {start}: {e}") + if response.text: + print(f"LinkedIn API Response: {response.text}") + break + + print(f"\n🎉 Finished! Successfully retrieved a total of {len(all_posts)} posts.") + return all_posts + +# --- Run the application --- +if __name__ == "__main__": + posts = get_all_linkedin_posts(ACCESS_TOKEN, ORGANIZATION_ID) + + # Print a quick preview of the first 3 posts + if posts: + print("\n--- Preview of latest 3 posts ---") + for post in posts[:3]: + # Safely extract the text content + text = post.get('commentary', {}).get('text', 'No text content') + print(f"ID: {post.get('id')}") + print(f"Content: {text[:100]}...\n") diff --git a/media/audio-extractor.sh b/media/audio-extractor.sh new file mode 100755 index 0000000..998c584 --- /dev/null +++ b/media/audio-extractor.sh @@ -0,0 +1,36 @@ +#!/bin/bash + + +# Check if ffmpeg is installed +if ! command -v ffmpeg &> /dev/null +then + echo "ffmpeg could not be found. Please install ffmpeg first." + exit 1 +fi + +# Check for correct number of arguments +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Input and output files +INPUT_FILE="$1" +OUTPUT_FILE="$2" + +# Check if the input file exists +if [ ! -f "$INPUT_FILE" ]; then + echo "Input file '$INPUT_FILE' does not exist." + exit 1 +fi + +# Extract MP3 from MP4 +ffmpeg -i "$INPUT_FILE" -q:a 0 -map a "$OUTPUT_FILE" + +# Check if the operation was successful +if [ $? -eq 0 ]; then + echo "MP3 file successfully created: $OUTPUT_FILE" +else + echo "Failed to extract MP3 from $INPUT_FILE." + exit 1 +fi diff --git a/media/convert_mp4_to_webm.sh b/media/convert_mp4_to_webm.sh new file mode 100755 index 0000000..7a0ca61 --- /dev/null +++ b/media/convert_mp4_to_webm.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Function to convert MP4 to WebM +convert_mp4_to_webm() { + input_file="$1" + output_file="$2" + + # Check if input file exists + if [ ! -f "$input_file" ]; then + echo "Error: File '$input_file' does not exist." + exit 1 + fi + + # Check if the input file is an MP4 + if [[ "$input_file" != *.mp4 ]]; then + echo "Error: Input file must be an MP4 file." + exit 1 + fi + + # Set output file name if not provided + if [ -z "$output_file" ]; then + output_file="${input_file%.mp4}.webm" + fi + + # Convert the MP4 to WebM using ffmpeg + ffmpeg -i "$input_file" -c:v libvpx -c:a libvorbis "$output_file" + + # Check if conversion was successful + if [ $? -eq 0 ]; then + echo "Successfully converted to '$output_file'." + else + echo "Error: Conversion failed." + exit 1 + fi +} + +# Check for input arguments +if [ $# -lt 1 ]; then + echo "Usage: $0 [output_file]" + exit 1 +fi + +# Call the function with arguments +convert_mp4_to_webm "$1" "$2" + diff --git a/media/copy_cbr_files.sh b/media/copy_cbr_files.sh new file mode 100644 index 0000000..727e210 --- /dev/null +++ b/media/copy_cbr_files.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Destination directory for .cbr files +DEST_DIR="cbr_files" + +# Create the destination directory if it doesn't exist +mkdir -p "$DEST_DIR" + +# Find all .cbr files recursively in volum_* directories and copy them to cbr_files +find volum_* -type f -name "*.cbr" -exec cp {} "$DEST_DIR" \; + +echo "All .cbr files have been copied to $DEST_DIR." \ No newline at end of file diff --git a/media/download_instagram_images.sh b/media/download_instagram_images.sh new file mode 100755 index 0000000..34a2043 --- /dev/null +++ b/media/download_instagram_images.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Script to download images from an Instagram post +# Requires: curl, jq + +# Function to display usage +function usage() { + echo "Usage: $0 " + exit 1 +} + +# Check if URL is provided +if [ $# -ne 1 ]; then + usage +fi + +# Instagram post URL +POST_URL=$1 + +# Fetch the HTML of the Instagram post +echo "Fetching post data..." +HTML=$(curl -s -L "$POST_URL") + +# Extract image URLs from the HTML +echo "Extracting image URLs..." +IMAGE_URLS=$(echo "$HTML" | sed -n 's/.*"display_url":"\([^"]*\)".*/\1/p') + +# Check if any image URLs were found +if [ -z "$IMAGE_URLS" ]; then + echo "Failed to extract image URLs. Make sure the URL is valid and public." + exit 1 +fi + +# Create a directory to save images +SAVE_DIR="instagram_images" +mkdir -p "$SAVE_DIR" + +# Download each image +echo "Downloading images..." +COUNT=1 +for URL in $IMAGE_URLS; do + FILE_NAME="$SAVE_DIR/image_$COUNT.jpg" + curl -s -o "$FILE_NAME" "$URL" + echo "Downloaded: $FILE_NAME" + ((COUNT++)) +done + +echo "All images downloaded to $SAVE_DIR." \ No newline at end of file diff --git a/media/export_instagram_cookies.py b/media/export_instagram_cookies.py new file mode 100644 index 0000000..57c0bf3 --- /dev/null +++ b/media/export_instagram_cookies.py @@ -0,0 +1,22 @@ +# export_instagram_cookies.py +import browser_cookie3 + +cj = browser_cookie3.chrome(domain_name='instagram.com') + +with open('cookies.txt', 'w') as f: + f.write("# Netscape HTTP Cookie File\n") + f.write("# This file was generated by browser-cookie3\n") + f.write("# https://curl.se/docs/http-cookies.html\n\n") + for cookie in cj: + # domain, flag, path, secure, expiration, name, value + f.write( + f"{cookie.domain}\t" + f"{'TRUE' if cookie.domain.startswith('.') else 'FALSE'}\t" + f"{cookie.path}\t" + f"{'TRUE' if cookie.secure else 'FALSE'}\t" + f"{int(cookie.expires) if cookie.expires else 0}\t" + f"{cookie.name}\t" + f"{cookie.value}\n" + ) +print("cookies.txt exported!") +# This script exports Instagram cookies from Chrome to a cookies.txt file. \ No newline at end of file diff --git a/media/image-instagram-downloader.sh b/media/image-instagram-downloader.sh new file mode 100755 index 0000000..7eea653 --- /dev/null +++ b/media/image-instagram-downloader.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Directory to save downloaded images +OUTPUT_DIR="./downloads" +mkdir -p "$OUTPUT_DIR" + +# Instagram Full Image Downloader +# Usage: ./image-instagram-downloader.sh + +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +URL="$1" + +# Fetch HTML content of the Instagram post +HTML_DATA=$(curl -s "$URL") + +if [ -z "$HTML_DATA" ]; then + echo "Could not fetch HTML data." + exit 2 +fi + +# Extract JSON data containing the full-resolution image URL +JSON_DATA=$(echo "$HTML_DATA" | grep -oE '