{"id":21836021,"url":"https://github.com/nocursor/saucexages","last_synced_at":"2025-06-15T15:32:47.939Z","repository":{"id":57545948,"uuid":"155727132","full_name":"nocursor/saucexages","owner":"nocursor","description":"Elixir SAUCE library for reading, writing, fixing, introspecting, and building SAUCE-aware applications.","archived":false,"fork":false,"pushed_at":"2018-11-02T18:47:52.000Z","size":638,"stargazers_count":10,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-08T13:51:18.742Z","etag":null,"topics":["ansi","ansi-art","art","ascii-art","elixir","elixir-lang","elixir-language","metadata","sauce","scene","steganography"],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nocursor.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-11-01T14:24:40.000Z","updated_at":"2025-01-28T16:20:39.000Z","dependencies_parsed_at":"2022-09-26T21:41:10.039Z","dependency_job_id":null,"html_url":"https://github.com/nocursor/saucexages","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nocursor%2Fsaucexages","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nocursor%2Fsaucexages/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nocursor%2Fsaucexages/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nocursor%2Fsaucexages/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nocursor","download_url":"https://codeload.github.com/nocursor/saucexages/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248852427,"owners_count":21171894,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ansi","ansi-art","art","ascii-art","elixir","elixir-lang","elixir-language","metadata","sauce","scene","steganography"],"created_at":"2024-11-27T20:32:04.360Z","updated_at":"2025-04-14T09:23:20.458Z","avatar_url":"https://github.com/nocursor.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Saucexages\n\n```\n                            ______        ______\n                            \\ .: /________\\ :. /\n                             \\___    .     ___/\n                               /     |      \\\n+--Saucexages!----------------/      :____   \\----Elixir-SAUCE-Library---------+\n   __________________________/_______|    \\___\\_____________________________\n   \\___     _____________   _____    |       ___________     \\_  _______    \\\n  ___/  _____    \\     _:      \\_    :         \\       |______/      |______/\n  \\________  \\    \\____\\         \\_______       \\___   :      \\___   :      \\_\n+---------\\_________/---\\_________/----\\_________/-\\___________/-\\___________/-+\n```\n\nSaucexages is a library for reading, writing, analyzing, introspecting, and managing [SAUCE](http://www.acid.org/info/sauce/sauce.htm).\n\n[SAUCE](http://www.acid.org/info/sauce/sauce.htm) is a standard used for attaching metadata to files. The SAUCE format was most commonly found in the [ANSi Art](https://en.wikipedia.org/wiki/ANSI_art) scene and generally various underground media scenes.\n\nIf the wordplay still escapes you, \"sauce\" was a cheeky way of saying \"source\", long before the term entered into internet meme territory. Memes aside, SAUCE can be thought of a way of attaching source information to binary.\n\n## Use-Cases\n\nSAUCE should generally be only used for `file` and `data types` it supports. If you want to use SAUCE, be sure to read the [SAUCE specification](http://www.acid.org/info/sauce/sauce.htm) to you fully understand the implications and reasons why or why not to add SAUCE to your data.\n\nCommon use-cases for SAUCE include:\n\n* Adding author, group, title, and other media specific information to files\n* Augmenting a file format's native metadata capabilities.\n* Compatibility with SAUCE-aware software and tools such as [ANSi editors](http://picoe.ca/products/pablodraw/), [BBSs](https://en.wikipedia.org/wiki/Bulletin_board_system), [Trackers](https://en.wikipedia.org/wiki/Music_tracker), and format viewers among other possibilities.\n* Finger-printing to help combat ripping, copying, and stealing of media.\n\nSAUCE is commonly found in the wild in some of these places (not limited to):\n\n* [ANSi art packs](https://github.com/sixteencolors/sixteencolors-archive) and associated files (ascii, bitmaps, music, literature)\n* Computer Music such MODs, S3Ms, and IT files\n* [ASCII art](https://files.scene.org/browse/graphics/ascii/)\n* Vector art such as [RIP graphics](https://en.wikipedia.org/wiki/Remote_Imaging_Protocol)\n\n## Usage\n\nThe most typical usages of Saucexages are reading, writing, removing, and checking SAUCE data.\n\nGiven an ANSI such as the following shown in a butchered screen shot below, let's have a look at the data attached:\n\n![lord jazz ansi art](https://raw.githubusercontent.com/nocursor/saucexages/master/docs/assets/ld-ansi.jpg)\n\nLet's read the data stored in this ANSI:\n\n```elixir\nFile.read!(\"docs/assets/LD-PARA1.ANS\") \n|\u003e Saucexages.sauce()  \n{:ok,\n %Saucexages.SauceBlock{\n   author: \"Lord Jazz\",\n   comments: [\"Saucexages put this comment here as a test!\"],\n   date: ~D[1995-03-17],\n   group: \"ACiD Productions\",\n   media_info: %Saucexages.MediaInfo{\n     data_type: 1,\n     file_size: 52020,\n     file_type: 1,\n     t_flags: 0,\n     t_info_1: 80, \n     t_info_2: 216,\n     t_info_3: 16,\n     t_info_4: 0,\n     t_info_s: \"IBM VGA\"\n   },\n   title: \"Parallox\",\n   version: \"00\"\n }}\n\n```\n\nLet's get some further detail that might be relevant to a viewer, search engine, etc:\n\n```elixir\nFile.read!(\"docs/assets/LD-PARA1.ANS\") \n|\u003e Saucexages.details()\n{:ok,\n %{\n   ansi_flags: %Saucexages.AnsiFlags{\n     aspect_ratio: :none,\n     letter_spacing: :none,\n     non_blink_mode?: false\n   },\n   author: \"Lord Jazz\",\n   character_width: 80,\n   comments: [\"Saucexages put this comment here as a test!\"],\n   data_type: 1,\n   data_type_id: :character,\n   date: ~D[1995-03-17],\n   file_size: 52020,\n   file_type: 1,\n   font_id: :ibm_vga,\n   group: \"ACiD Productions\",\n   media_type_id: :ansi,\n   name: \"ANSi\",\n   number_of_lines: 216,\n   t_info_3: 16,\n   t_info_4: 0,\n   title: \"Parallox\",\n   version: \"00\"\n }}\n \n```\n\nThe same data, but viewed in an ANSI drawing app, [Pablo Draw](http://picoe.ca/products/pablodraw/):\n\n![lord jazz ansi sauce in pablo draw](https://raw.githubusercontent.com/nocursor/saucexages/master/docs/assets/pablo-sauce.jpg)\n\nNote that much of the above data is dependent on the `file_type` and `data_type` field. For instance, note the `iCE Colors` and `Legacy Aspect Ratio` fields. These fields are specific to some media types and must be interpreted from the base `t_XXX` fields. If we were working with an audio file instead, other fields such as `sample_rate` would need to be interpreted and displayed instead.\n\nIn other words, the UI would need to change depending on the meaning of these fields which can vary. Calling `Saucexages.details/1` is one of many ways to extract such data. See `Saucexages.MediaInfo` for further functionality. \n\n\nWriting a SAUCE block:\n\n```elixir\nsauce_block =  %Saucexages.SauceBlock\n{\n author: \"Hamburgler\",\n comments: [\"I take credit for this ANSI as my own!\"],\n date: ~D[2018-06-01],\n group: \"Shady Activities\",\n media_info: %Saucexages.MediaInfo{\n   data_type: 1,\n   file_size: 52020,\n   file_type: 1,\n   t_flags: 0,\n   t_info_1: 80, \n   t_info_2: 500,\n   t_info_3: 0,\n   t_info_4: 0,\n   t_info_s: \"Amiga Topaz 1+\"\n },\n title: \"Donut Entry\",\n version: \"00\"\n}\n\n# normally you'd already have a bin in memory, here we just create a fake one for example purposes\nbin = \u003c\u003c1, 2, 3\u003e\u003e\n{:ok, updated_bin} = Saucexages.write(bin, sauce_block)\n\n```\n\nRemoving a SAUCE block:\n\n```elixir\nFile.read!(\"docs/assets/LD-PARA1.ANS\")\n|\u003e Saucexages.remove_sauce()\n{:ok,\n \u003c\u003c27, 91, 50, 53, 53, 68, 27, 91, 52, 48, 109, 13, 10, 27, 91, 48, 59, 49, 109,\n   97, 27, 91, 49, 48, 67, 27, 91, 48, 109, 67, 27, 91, 57, 67, 27, 91, 49, 59,\n   51, 48, 109, 105, 27, 91, 57, 67, 100, 27, ...\u003e\u003e}\n```\n\nRemoving SAUCE comments:\n\n```elixir\nFile.read!(\"docs/assets/LD-PARA1.ANS\")\n|\u003e Saucexages.remove_comments()\n{:ok,\n \u003c\u003c27, 91, 50, 53, 53, 68, 27, 91, 52, 48, 109, 13, 10, 27, 91, 48, 59, 49, 109,\n   97, 27, 91, 49, 48, 67, 27, 91, 48, 109, 67, 27, 91, 57, 67, 27, 91, 49, 59,\n   51, 48, 109, 105, 27, 91, 57, 67, 100, 27, ...\u003e\u003e}\n   \n```\n\nChecking for a SAUCE block:\n\n```elixir\nFile.read!(\"docs/assets/LD-PARA1.ANS\")\n|\u003e Saucexages.sauce?()\ntrue\n\n\u003c\u003c1, 2, 3\u003e\u003e |\u003e Saucexages.sauce?()\nfalse\n```\n\nChecking for a COMMENT block:\n\n```elixir\nFile.read!(\"docs/assets/LD-PARA1.ANS\")\n|\u003e Saucexages.comments?()\ntrue\n\n```\n\nWe can even separate the contents from the SAUCE\n```elixir\nFile.read!(\"docs/assets/LD-PARA1.ANS\")\n|\u003e Saucexages.contents()\n\n{:ok,\n \u003c\u003c27, 91, 50, 53, 53, 68, 27, 91, 52, 48, 109, 13, 10, 27, 91, 48, 59, 49, 109,\n   97, 27, 91, 49, 48, 67, 27, 91, 48, 109, 67, 27, 91, 57, 67, 27, 91, 49, 59,\n   51, 48, 109, 105, 27, 91, 57, 67, 100, 27, ...\u003e\u003e}\n```\n\nSometimes we might be working with larger files. We could do some of the work ourselves using the Elixir and Erlang `IO`and `file`\nAPIs, or we could be lazy and let Saucexages have a try:\n\n```elixir\nSaucexages.IO.FileReader.sauce(\"docs/assets/LD-PARA1.ANS\")\n{:ok,\n %Saucexages.SauceBlock{\n   author: \"Lord Jazz\",\n   comments: [\"Saucexages put this comment here as a test!\"],\n   date: ~D[1995-03-17],\n   group: \"ACiD Productions\",\n   media_info: %Saucexages.MediaInfo{\n     data_type: 1,\n     file_size: 52020,\n     file_type: 1,\n     t_flags: 0,\n     t_info_1: 80, \n     t_info_2: 216,\n     t_info_3: 16,\n     t_info_4: 0,\n     t_info_s: \"IBM VGA\"\n   },\n   title: \"Parallox\",\n   version: \"00\"\n }}\n\n# We can do everything we can do when working with binary as well, such as check for a SAUCE\n# This reads backwards by seeking to the end of the file and only loading in the necessary chunks, rather than a whole binary\nSaucexages.IO.FileReader.sauce?(\"docs/assets/LD-PARA1.ANS\")\ntrue\n\n# And for comments\nSaucexages.IO.FileReader.comments?(\"docs/assets/LD-PARA1.ANS\")\ntrue\n\n```\n\nWhat happens when we want to handle files that don't have a SAUCE?\n\n```elixir\n# no problem here, and we get a value we can pattern match against\nSaucexages.sauce(\u003c\u003c1, 2, 3\u003e\u003e)       \n{:error, :no_sauce}\n\n\nSaucexages.comments(\u003c\u003c1, 2, 3\u003e\u003e)       \n{:error, :no_sauce}\n\nSaucexages.details(\u003c\u003c1, 2, 3\u003e\u003e) \n{:error, :no_sauce}\n\n# we can safely remove things without worry\nSaucexages.remove_sauce(\u003c\u003c1, 2, 3\u003e\u003e)\n{:ok, \u003c\u003c1, 2, 3\u003e\u003e}\n  \n# and of course we can attach a SAUCE block where there was none\nsauce_block = %Saucexages.SauceBlock{\n                author: \"Lord Jazz\",\n                comments: [\"Saucexages put this comment here as a test!\"],\n                date: ~D[1995-03-17],\n                group: \"ACiD Productions\",\n                media_info: %Saucexages.MediaInfo{\n                  data_type: 1,\n                  file_size: 52020,\n                  file_type: 1,\n                  t_flags: 0,\n                  t_info_1: 80,\n                  t_info_2: 216,\n                  t_info_3: 16,\n                  t_info_4: 0,\n                  t_info_s: \"IBM VGA\"\n                },\n                title: \"Parallox\",\n                version: \"00\"\n              }\n\nSaucexages.write(\u003c\u003c1, 2, 3\u003e\u003e, sauce_block)\n\n{:ok,                                                                              \n \u003c\u003c1, 2, 3, 26, 67, 79, 77, 78, 84, 83, 97, 117, 99, 101, 120, 97, 103, 101,\n   115, 32, 112, 117, 116, 32, 116, 104, 105, 115, 32, 99, 111, 109, 109, 101,\n   110, 116, 32, 104, 101, 114, 101, 32, 97, 115, 32, 97, 32, 116, ...\u003e\u003e}\n   \n# notice in the return that we see \u003c\u003c26, 67, 79, 77, 78, 79\u003e\u003e as a sequence before other data\n# This is our comments block, with an EOF character in front of it\n\u003c\u003c67, 79, 77, 78, 84\u003e\u003e\n\"COMNT\"\n   \n```\n\nLets learn a bit about SAUCE by via a small preview of working with some meta information about SAUCE:\n\n```elixir\nrequire Saucexages.Sauce\n\n# What is the SAUCE record ID field in a binary?\nSaucexages.Sauce.sauce_id()\n\"SAUCE\"\n\n# What is the comments block ID field in a binary?\nSaucexages.Sauce.comment_id()\n\"COMNT\"\n\n# What is the default value for the SAUCE version?\nSaucexages.Sauce.sauce_version()\n\"00\"\n\n# How big is a SAUCE record in bytes?\nSaucexages.Sauce.sauce_record_byte_size()\n128\n\n# How many bytes of a SAUCE record is allocated to the actual data?\nSaucexages.Sauce.sauce_data_byte_size\n123\n\n# How large is the smallest comments block in bytes?\nSaucexages.Sauce.minimum_comment_block_byte_size()\n69\n\n# How many bytes can a single comment line fit?\nSaucexages.Sauce.comment_line_byte_size()\n64\n\n# What about a comments block with 10 comments in bytes?\nSaucexages.Sauce.comment_block_byte_size(10)\n645\n\n# How many bytes do we need to store a SAUCE block with 10 comments?\nSaucexages.Sauce.sauce_byte_size(10)\n773\n\n# How many bytes maximum can a title hold?\nSaucexages.Sauce.field_size(:title)\n35\n\n# What is the offset in a SAUCE record for the group field?\nSaucexages.Sauce.field_position(:group)\n62\n\n# What is the maximum number of comment lines allowed?\nSaucexages.Sauce.max_comment_lines()\n255\n\n# What are the required fields?\nSaucexages.Sauce.required_field_ids()\n[:sauce_id, :version, :data_type, :file_type]\n\n# Can I use things like field size to build binaries? Yes you can.\n# Let's implement the world's most naive SAUCE reader\nalias Saucexages.Sauce\nbin = File.read!(\"docs/assets/LD-PARA1.ANS\")\n\n \u003c\u003cSauce.sauce_id(),\n   version::binary-size(Sauce.field_size(:version)),\n   title::binary-size(Sauce.field_size(:title)),\n   author::binary-size(Sauce.field_size(:author)),\n   group::binary-size(Sauce.field_size(:group)),\n   date::binary-size(Sauce.field_size(:date)),\n   file_size::binary-size(Sauce.field_size(:file_size)),\n   data_type::little-unsigned-integer-unit(8)-size(Sauce.field_size(:data_type)),\n   file_type::little-unsigned-integer-unit(8)-size(Sauce.field_size(:file_type)),\n   t_info_1::binary-size(Sauce.field_size(:t_info_1)),\n   t_info_2::binary-size(Sauce.field_size(:t_info_2)),\n   t_info_3::binary-size(Sauce.field_size(:t_info_3)),\n   t_info_4::binary-size(Sauce.field_size(:t_info_4)),\n   comment_lines::binary-size(Sauce.field_size(:comment_lines)),\n   t_flags::binary-size(Sauce.field_size(:t_flags)),\n   t_info_s::binary-size(Sauce.field_size(:t_info_s)),\n \u003e\u003e = :binary.part(bin, byte_size(bin), -128) \n\ntitle\n\"Parallox                           \"\n \n```\n\nA small preview of working with Media:\n\n```elixir\nrequire Saucexages.MediaInfo\n\n# Translate file type and data type to something human readable\nSaucexages.MediaInfo.media_type_id(1, 1)\n:ansi\n\n# What's the file type used by SAUCE to store an s3m?\nSaucexages.MediaInfo.file_type(:s3m)  \n3\n\n# What file types are valid for a character data type?\nSaucexages.MediaInfo.file_types_for(:character)        \n[0, 1, 2, 3, 4, 5, 6, 7, 8]\n\n# What's the data type for a png?\nSaucexages.MediaInfo.data_type(:png) \n2\n\n# Let's work more directly with media info that we may have grabbed from a SAUCE\n media_info = %Saucexages.MediaInfo{\n   data_type: 1,\n   file_size: 52020,\n   file_type: 1,\n   t_flags: 16,\n   t_info_1: 80, \n   t_info_2: 500,\n   t_info_3: 0,\n   t_info_4: 0,\n   t_info_s: \"Amiga Topaz 1+\"\n }\n\n# Let's look at some basic info about our data\nSaucexages.MediaInfo.basic_info(media_info)                        \n%{data_type_id: :character, media_type_id: :ansi, name: \"ANSi\"}\n\n# Which fields for an ANSI are type dependent and can be translated?\nSaucexages.MediaInfo.type_fields(:ansi)     \n[:t_flags, :t_info_1, :t_info_2, :t_info_s]\n\n# Let's translate only our flags\nSaucexages.MediaInfo.t_flags(media_info)\n{:ansi_flags,\n %Saucexages.AnsiFlags{\n   aspect_ratio: :modern,\n   letter_spacing: :none,\n   non_blink_mode?: false\n }}\n\n# Let's translate t_info_1 and t_info_2 in a single call\n Saucexages.MediaInfo.read_fields(media_info, [:t_info_1, :t_info_2])\n%{character_width: 80, number_of_lines: 500}\n\n# Let's just fully translate everything\n Saucexages.MediaInfo.details(media_info)\n%{\n  ansi_flags: %Saucexages.AnsiFlags{\n    aspect_ratio: :modern,\n    letter_spacing: :none,\n    non_blink_mode?: false\n  },\n  character_width: 80,\n  data_type: 1,\n  data_type_id: :character,\n  file_size: 52020,\n  file_type: 1,\n  font_id: :amiga_topaz_1_plus,\n  media_type_id: :ansi,\n  name: \"ANSi\",\n  number_of_lines: 500,\n  t_info_3: 0,\n  t_info_4: 0\n}\n\n```\n\nA small preview of working with Fonts:\n\n```elixir\nrequire Saucexages.Font\n\n# Get the font name used in a SAUCE record\nSaucexages.Font.font_name(:ibm_vga)\n\"IBM VGA\"\n\n# Get a known font id from its string representation\nSaucexages.Font.font_id(\"Amiga Topaz 1+\")  \n:amiga_topaz_1_plus\n\n# Get some basic info about a font to help with display\nSaucexages.Font.font_info(:ibm_vga)\n%Saucexages.FontInfo{\n  encoding_id: :cp437,\n  font_id: :ibm_vga,\n  font_name: \"IBM VGA\"\n}\n\n# Check what fonts are available for a given font id\nSaucexages.Font.font_options(:ibm_vga)   \n[\n  %Saucexages.FontOption{\n    font_id: :ibm_vga,\n    properties: %Saucexages.FontProperties{\n      display: {4, 3},\n      font_size: {9, 16},\n      pixel_ratio: {20, 27},\n      resolution: {720, 400},\n      vertical_stretch: 35.0\n    }\n  },\n  %Saucexages.FontOption{\n    font_id: :ibm_vga,\n    properties: %Saucexages.FontProperties{\n      display: {4, 3},\n      font_size: {8, 16},\n      pixel_ratio: {6, 5},\n      resolution: {640, 400},\n      vertical_stretch: 20.0\n    }\n  }\n]\n\n```\n\n## Features\n\nSaucexages provides numerous functions and modules for working with SAUCE. Some major highlights include:\n\n* Read and write SAUCE from both file paths and in-memory binaries\n* Add/Remove SAUCE comments\n* Update individual or all SAUCE fields\n* Remove SAUCE records/clean files\n* Fix broken SAUCE records and comments\n* Support for all file type-specific fields in the SAUCE spec\n* Interrogate metadata in a human-readable format\n* Encode/decode specialty fields such as ANSi flags (ex: ICE Colors), fonts, pixel depth, aspect ratio, resolution, vertical stretch, letter spacing, sample rate, and more.\n* Support for all media types in the SAUCE spec including bitmaps, audio files, archives, executables among others.\n* Read SAUCE data in a tolerant manner that handles common mistakes found in the real-world\n* Handle large files\n* Offer SAUCE related constants, calculations, and more via macros and compile time features, or otherwise efficiently.\n* Eliminate the need for passing around magic numbers and constants for sizes, offsets, and more when working with SAUCE.\n* Encodes and decodes strings using correct code pages.\n\n## Installation\n\nSaucexages is available via [Hex](https://hex.pm/packages/saucexages). The package can be installed by adding `saucexages` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:saucexages, \"~\u003e 0.2.0\"}\n  ]\nend\n```\n\n## Documentation\n\nAdditional documentation including API docs with examples can be be found at [https://hexdocs.pm/saucexages](https://hexdocs.pm/saucexages) and in the `docs` folder.\n\n* [Overview](docs/overview.md) - An overview of this library with some further detail including goals, limitations, and other topics.\n\n* [Rationale](docs/rationale.md) - Why this library was created\n\n* [FAQ](docs/FAQ.md) - Additional questions, fun stuff, and background.\n\n## Acknowledgments\n\n* [ACiD Productions](http://www.acid.org/) - Creators of SAUCE\n* Oliver \"Tasmaniac\" Reubens / ACiD - ACiD member, contributions to SAUCE and SAUCE spec.\n* [PabloDraw](http://picoe.ca/products/pablodraw/) - Demonstration of SAUCE in a UI. \n* All test data such as ANSi art, ASCII art, and music is copyright the original authors.\n\nPlease support online art scenes.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnocursor%2Fsaucexages","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnocursor%2Fsaucexages","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnocursor%2Fsaucexages/lists"}