Your favorite catgirl's projects 26-03-30: Cables.GL, what's that ?! Good morning beautiful people reading this ! I often mention cables.gl , but what is it ???? Well here's what their website says : Visual Programming with cables Cables is a tool for creating beautiful interactive content. With an easy to navigate interface and real time visuals, it allows for rapid prototyping and fast adjustments. You are provided with a set of operators, such as mathematical functions, shapes, materials and post processing effects. Connect these to each other with virtual cables to create the experience you have in mind. Easily export your piece of work at any time. Embed it into your website or use it for any kind of creative installation. I will not get too technical in this blog post, gonna be short and mainly to showcase stuff i already made :3 Where's VVVV ? In my first blog post here i used VVVV; And i used it quite a bunch more after that! very cool software.. But after switching completly to linux well... it just not worth the effort to get it running smoothly, every solution i tried resulted in at least one really annoying bug that made the experience a pain. Searching for an alternative So i wondered, is there anything else like VVVV ? I tried a few, puredata/plugdata - TouchDesigner - others you can find on here : https://alternativeto.net/software/vvvv/ Then i came across cables.gl, at first redo-ing my project on there wasn't really straight forward, linkage order between nodes is much more important than in VVVV, and for some, a bit less intuitive (thanks to the online examples and ressources i did it!) In 4V i was able to use the sound output of the pc directly, AFAIK there is no easy way of doing it in cables.gl, i could do a sink that sends it back to an input but am lazy and so i just went with the "media player" approach at that time First project, music visualiser with a fixed music and text https://public.frezlotte.ch/cables/music_visualiser_old/ There's a lot of keybinds u have to know to use (Q-A; W-S; E-D; clicks and scroll), and parameters are not clamped (stopping to a min or a max) so u can easily make it look like crap or stop rendering, but as a first project, not bad :3 Also it made me learn that not everything translates well on a browser, for some reason at that time the multi-ports nodes worked fine in editor but broke everything in a webpage, really painful for a newcomer. Second project, new year clock https://public.frezlotte.ch/cables/year_countdown/ New year was coming fast and i wanted to do something, either a mini game / activity or idk, something For some reasons i won't go into details about, i wasn't sure with who i would spend my NY eve, but shortly after getting an idea of some "plans", i got to work on the timer to put on a tablette somewhere At first i really wanted to put a rick roll or SOMETHING at 00:00, that would had been fun, but i ran out of time and just made a quick "HNY" and made the background go a tad faster, that's it. Looking back a lot of ppl used it so i would had felt bad rickrolling all their families... i won't do it this year ! no promises tho :3 Second project, the sequel Worked well for new year, but i wasn't gonna stop there, it's a gimmick but the background was really pretty, even at that time i wanted more control and settings, and i came across the notion of sidebar This little fella, you can toggle it at anytime top right of the browser, in there i made multiple pages, and tbh, am quite happy with how it is right now Using it i was able to find some very pretty settings really fast, using vvvv i thought of it as a menu you did after the fact, but in cables.gl, having it along the making of something is really useful to be prototyping in no time Sorry for the mess ! Here's the menus in node form, that's the entirety of the sidebar right there. Top right is a future plans, import and exports of settings. Here's for the visual part of it Seeing it again today, i would changes a looooot of things, but hey it means i progressed so yay Performance wise you can bet it runs like a burning trash can. Optimising ??? Yo girl that's not a real word what are you inventing rn ?? Third project, frezlotte.ch I always wanted a fancy, artsy landing page for https://frezlotte.ch , and now, i got one ! My latest project in date, went through a number of iterations, but am very happy about the end result ! The glitches, the tv effects, the visual, exactly like i pictured it in my mind before actually doing it, maybe even better :3 Looks messy but i promise i have a reason !! When i started this project it was cause i could use variables at all, am not sure what build i was on but even for the time.frezlotte.ch website i simply could use any, even in new files So this project, at first, was done with no variables (that was horrible), especially the green thingy that goes left and right, that's Mouse movements, and with variables it would had been chucked in a corner, but now it's kinda difficult to tidy up without changing any behaviours... For that website i had to do a lot of key mapping, up-down scroll-up scroll-down, space enter click and touch control too ! Was quite easy, and i made it so that the texture isn't reloaded for nothing so it's way better optimised, still capped fps to 24 cause that's the vibe i was going for. That's pretty much it, won't go into specifics but it's an introduction to that subject, below i'll post progress of different iterations of the website so you can see how it evolved. Take care of yourself ! 🩵🩷🤍🩷🩵 26-03-06: Kwutty, my public instance of kutty to shorten links kwutty - URL, free public URL shortener Someone on discord spoke about targeting a qr-code to as linktree so that we are sure we can always make sure the target is available and we don't need to reprint some cards. So it got me thinking, i'm currently doing annoying HTTP only redirect that ppl often open as HTTPS and gets a warning for, plus it's not very readable (and it's publicly available on site like https://crt.sh/?q=frezlotte.ch ) so i digged a little using my trusty https://alternativeto.net to find a self hosted link shortener service, with telemetry, moderation, and the possibility to make link password protected. came across https://kutt.to/ , very well build and easy to setup, the compose.yml is as follow : services: kutt: build: context: . volumes: - ./db_data_sqlite:/var/lib/kutt - ./custom:/kutt/custom environment: DB_FILENAME: "/var/lib/kutt/data.sqlite" networks: - ${T_NETWORK:-tproxy} labels: - "traefik.enable=true" - "traefik.docker.network=tproxy" - "traefik.http.routers.${T_SERVICE_NAME}.rule=Host(`${T_SERVICE_SUBDOMAIN}.frezlotte.ch`) || Host(`${T_SERVICE_SUBDOMAIN}.zpy.ch`)" - "traefik.http.routers.${T_SERVICE_NAME}.entrypoints=${T_ENTRYPOINT:-websecure}" - "traefik.http.routers.${T_SERVICE_NAME}.tls.certresolver=myresolver" - "traefik.http.routers.${T_SERVICE_NAME}.service=kwutty" - "traefik.http.routers.${T_SERVICE_NAME}.middlewares=${T_MIDDLEWARES:- bouncer, umami, umami-feeder, anubis}" - "traefik.http.services.${T_SERVICE_NAME}.loadbalancer.server.port=${T_SERVICE_PORT}" networks: tproxy: external: true Like the usual, i'm using traefik , with all my usual middlewares I'm probably add a ipallowlist middleware on JUST the admin path but i'm not sure how doable it is yet There's a umami option in kutt but i decided to not use it since i have a umami-feeder (automatically register websites in umami) so the work was already working. The admin page listing link looks like that : Simple view count, and if you click on the purple button, you get there : I'll do a umami blog post separately, but this was kutty, as Kwutty Take care of yourself ! 🩵🩷🤍🩷🩵 26-03-04: listening to banger song as i'm writing this Bad code ahead, be warned !! (please dont tell my mom about it ....) n8n music history - l.frezlotte.ch http redirect to https://n8n.frezlotte.ch/form/7766c9d4-aff1-452a-b455-a50d964793ac but it's a bit much to remember i think :3 Landing page of the site, tf is that ? It's a n8n form, cause i don't want to do work constantly, pressing the button does the query and generate a static website That's nor good nor useful but i thought it was funny, so i did Website theme palette is https://coolors.co/230614-460c28-f0a6ca-efc3e6-f0e6ef So what does it do exactly Full form workflow Simple form setup from n8n, Title description and some custom css and that's it Then we get rows from a table we created manually before, settings are straight forward, we want all records and order them by id SQL queries to modify stuff, it's ugly and the "kinda" SQL-Lite sql-99 "almost" pisses me the f off but at least it works now: 32 last song listened to SELECT concat('

', db.SongArtiste, '

', db.SongName, '

', '

', '
', '
' ) AS HTLM_TEXT FROM input1 as db ORDER BY db.id desc LIMIT 32 ; Table at the bottom of the page - count SELECT SongArtiste AS Artiste, COUNT(*) AS ArtisteOccurrence, COUNT(DISTINCT SongName) AS UniqueMusicCount FROM input1 GROUP BY SongArtiste ORDER BY ArtisteOccurrence DESC Table to HTML SELECT CONCAT( '', '', db.Artiste, '', '', '', '', db.ArtisteOccurrence, '', '', db.UniqueMusicCount, '', '', MID(db.MostListenSong, 1, 75), IF(LEN(MID(db.MostListenSong, 1, 75)) > 70, "....", ""), '', '' ) as HTML_Code FROM input1 as db limit 50 ; Then we use the SUMMARIZE not to take each line and append them in one single string we merge both output in one JSON object so we can use it at the same time to do the last step HTML page generation with n8n's form we can redirect to a site, or just send back raw HTML Full template code Cha listen history

Chamallow's listening history

click up here to refresh the page

{{ $json.concatenated_HTLM_TEXT }}
{{ $json.concatenated_HTML_Code }}
Artiste listen Total Unique Most listened Title
important stuff are : {{ $json.concatenated_HTLM_TEXT }} {{ $json.concatenated_HTML_Code }} My names are shit but HTLM_TEXT (typo ik lmao) does the history on the page, and the HTML_Code is for the table Can't bother Feeding the table with songs akshtually Webhooks Like i did before with minecraft to have a 2 way chat system, i used webhooks, n8n is good for that you can easily setup data and have some ipAllowList so that not everyone can just POST on it (or you can also use diff kind of Auths, pretty based) Very simple stuff Automated POST requests for TIDAL i started from https://github.com/titaniumfish/tidal-discord-richpresence It missed a lot of features tho, and i really wanted to display images, tho i had an issue On Tidal, music can have some NSFW artwork, and i knew that wouldn't be okay with discord... So i added A module to call Tidal API, to see songs metadata i'm missing, and as a middle ground i show the album cover in the small icon using the "/icon" endpoint so 80x80, should be okay -> tho i saw since then that some song have NSFW artwork but no E rating, that's annoying but not much i can do about it Am putting the code below, it's bad and i vibe coded some of it but it works :3 cya 🩵🩷🤍🩷🩵 #!/usr/bin/env python3 import json import logging import platform import re import subprocess import time from pathlib import Path from typing import Any, Dict, Optional import requests from pypresence import Presence from pypresence.types import ActivityType, StatusDisplayType from tidalapi import Session from tidalapi.media import Track DISCORD_CLIENT_ID = "1477481718729674793" TIDAL_API_BASE = "http://localhost:47836" TIDAL_CURRENT_ENDPOINT = f"{TIDAL_API_BASE}/current" TIDAL_CURRENT_IMAGE = f"{TIDAL_API_BASE}/current/image" UPDATE_INTERVAL = 5 # seconds between updates CONNECT_RETRY_INTERVAL = 30 # seconds between Discord connection attempts def report_to_n8n(share_url, song_name, song_artiste, is_liked, song_art): payload = { "share_url": share_url, "song_name": song_name, "song_artiste": song_artiste, "is_liked": is_liked, "song_art": song_art, } try: response = requests.post( "https://user:pswrd@n8n.frezlotte.ch/webhook/ad127f87-cbae-4fde-800b-23b6e3f6a6f6", json=payload, timeout=5, ) response.raise_for_status() except requests.exceptions.RequestException as e: pass class TidalDiscordRPC: def __init__(self, tidal_api): self.discord_rpc = None self.last_track_data = None self.connected_to_discord = False self.api = tidal_api logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) self.logger = logging.getLogger(__name__) # logging.getLogger().setLevel(logging.DEBUG) def connect_discord(self) -> bool: try: if self.discord_rpc: self.discord_rpc.close() self.discord_rpc = Presence(DISCORD_CLIENT_ID) self.discord_rpc.connect() self.connected_to_discord = True self.logger.info("Connected to Discord Rich Presence") return True except Exception as e: self.logger.error(f"Failed to connect to Discord: {e}") self.connected_to_discord = False return False def get_current_track(self) -> Optional[Dict[str, Any]]: try: response = requests.get(TIDAL_CURRENT_ENDPOINT, timeout=5) response.raise_for_status() data = response.json() if data.get("title") and data.get("artists"): return data else: return None except requests.exceptions.RequestException as e: self.logger.error(f"Failed to get track info from Tidal Hi-Fi: {e}") return None except json.JSONDecodeError as e: self.logger.error(f"Invalid JSON response from Tidal Hi-Fi: {e}") return None def format_time(self, seconds: int) -> str: minutes = seconds // 60 seconds = seconds % 60 return f"{minutes}:{seconds:02d}" def update_discord_presence(self, track_data: Dict[str, Any]) -> bool: if not self.connected_to_discord: return False try: title = track_data.get("title", "Unknown Track") url = track_data.get("url", "") track_id_match = re.search(r"/track/(\d+)", url) track_id = track_id_match.group(1) if track_id_match else None track_api_data: "Track" = self.api.track(track_id) share_url = track_api_data.share_url image = track_data.get("image", "tidal-logo") artists = track_data.get("artists", "Unknown Artist") status = track_data.get("status", "unknown") current_seconds = track_data.get("currentInSeconds", 0) duration_seconds = track_data.get("durationInSeconds", 0) is_explicit = track_api_data.explicit or False small_image = "play" if status == "playing" else "pause" if is_explicit: small_image = image image = "https://cdn-icons-png.flaticon.com/512/8035/8035031.png" bpm = track_api_data.bpm popu = track_api_data.popularity audio_qual = track_api_data.audio_quality is_liked = track_data.get("favorite") spacer = " - " liked_char = "💖" description = liked_char if is_liked else "♡?" description += " " description += f"{bpm}bpm" + spacer if bpm else "" description += f"{popu}⥮" + spacer if popu else "" description += f"{audio_qual}" if audio_qual else "" state = liked_char + " " if is_liked else "" state += "[E] " if is_explicit else "" state += f"{artists}{spacer}{title}" presence_data = { "activity_type": ActivityType.LISTENING, "state": state, "large_text": description, "status_display_type": StatusDisplayType.STATE, "buttons": [{"label": "url", "url": share_url}], "large_image": image, "small_image": small_image, "small_text": "Playing" if status == "playing" else "Paused", } if status == "playing" and duration_seconds > 0: current_time = time.time() start_time = current_time - current_seconds end_time = start_time + duration_seconds presence_data["start"] = int(start_time) presence_data["end"] = int(end_time) self.discord_rpc.update(**presence_data) self.logger.debug(f"Sent presence data: {presence_data}") report_to_n8n(share_url, title, artists, is_liked, track_data.get("image")) time_info = "" if current_seconds and duration_seconds: time_info = f" [{self.format_time(current_seconds)}/{self.format_time(duration_seconds)}]" self.logger.info( f"Updated Discord: {title} by {artists} ({status}){time_info}" ) return True except Exception as e: self.logger.error(f"Failed to update Discord presence: {e}") self.logger.error( f"Presence data that failed: {presence_data if 'presence_data' in locals() else 'N/A'}" ) self.connected_to_discord = False return False def clear_discord_presence(self): if self.connected_to_discord and self.discord_rpc: try: self.discord_rpc.clear() self.logger.info("Cleared Discord presence") except Exception as e: self.logger.error(f"Failed to clear Discord presence: {e}") def has_track_changed(self, current_data: Dict[str, Any]) -> bool: if not self.last_track_data: return True current_key = ( current_data.get("favorite"), current_data.get("title"), current_data.get("artists"), current_data.get("status"), ) last_key = ( self.last_track_data.get("favorite"), self.last_track_data.get("title"), self.last_track_data.get("artists"), self.last_track_data.get("status"), ) return current_key != last_key def run(self): self.logger.info("Starting Tidal Hi-Fi Discord Rich Presence...") last_discord_attempt = 0 while True: try: current_time = time.time() if ( not self.connected_to_discord and (current_time - last_discord_attempt) > CONNECT_RETRY_INTERVAL ): self.connect_discord() last_discord_attempt = current_time track_data = self.get_current_track() if track_data and track_data.get("status") == "playing": if self.has_track_changed(track_data): if self.connected_to_discord: success = self.update_discord_presence(track_data) if not success: self.connected_to_discord = False self.last_track_data = track_data else: if self.last_track_data and self.connected_to_discord: self.clear_discord_presence() self.last_track_data = None time.sleep(UPDATE_INTERVAL) except KeyboardInterrupt: self.logger.info("Shutting down...") if self.connected_to_discord: self.clear_discord_presence() break except Exception as e: self.logger.error(f"Unexpected error: {e}") time.sleep(UPDATE_INTERVAL) if self.discord_rpc: self.discord_rpc.close() if __name__ == "__main__": # Wait until Discord (vesktop) and Tidal are running before proceeding while True: try: if platform.system() == "Windows": output = subprocess.check_output("tasklist", universal_newlines=True) discord_running = "Discord.exe" in output tidal_running = "Tidal.exe" in output else: output = subprocess.check_output(["ps", "aux"], universal_newlines=True) discord_running = "vesktop" in output tidal_running = "tidal-hifi" in output if discord_running and tidal_running: break except Exception: pass time.sleep(5) session_file1 = Path("tidal-session-oauthn.json") session = Session() session.login_session_file(session_file1) rpc = TidalDiscordRPC(session) rpc.run() 26-02-23: Waaaa, s t u f f AWO I changed ✨ stuff ✨ on the wiki now i'm gonna do blogs and research stuff on here gonna be so coooooool - Cha 🩵🩷🤍🩷🩵 25-07-16: me like music and cool visuals Heres 2 visuals i'm starting to be proud of, am really new to VVVV but i'm addicted already !!! Photosensible warning --- Flashing lights ahead !!! Technical details; I'm using my audient evo 4 - loopback, that i monitor to my headphones so i can listen at the same time, very useful i'm glad i didn't had to use virtual audio cable. Also tried to wire some "intruments" but it had random effects, cutting my mic, cutting my sound, sending the input back into my hears for some reason... I'll try again someday but for now it's fine with the loopback technique. Nodes and stuff if y'all want to see how it's done let me know, am not gonna write shit no one gonna read Music: Fireflies - Al'Tarba Music: THINGS WILL GET MUCH WORSE FROM HERE - Kill Bill - Rav and after a few more hours ...