StashTagSkins

:placard: Summary Used to quickly apply and change themes of tags for Stash.
:link: Repository https://github.com/Stash-KennyG/StashTagSkins/blob/main/StashTagSkins.ps1

StashTagSkins

Used to quickly apply and change themes of tags for Stash.

WARNING

While I will try to support this and fix any basic bugs, I am not responsible if this nukes your database. TAKE A BACKUP NOW! TAKE A BACKUP BEFORE YOUR RUN IT EACH TIME! The script does NOT delete any tags. The only potential data loss is in losing your tag images if they are cleared or overwritten, but please exercise good backup practice. Find me on discord if you have questions, but do not complain about something this did your stash. It works for me, and I have tried very hard to make it work, but this is best efforts - not commercial product.

Note: Disable authentication on stash to run the script. This is the number one issue people have reported with this on Discord. The current version does not do authentication.

Prerequisites

This script requires

Installing Powershell

Powershell runs on Linux, MacOS and Windows. You do not need to run this script from your stash instance. For convenience it is configured to look for localhost, but you can select any endpoint.

Installing PSGraphQL

By Default, you can use the built-in module manager to get PSGraphQL

Install-Module -Name PSGraphQL -Repository PSGallery -Scope CurrentUser

Executing

Open a powershell terminal and execute the main file to connect to a local stash

.\StashTagSkins.ps1

Or if you’d like you can specify an alternative stash

.\StashTagSkins.ps1  -StashAddress "192.168.1.100:9999"

Configuration

Upon launch, the script will ask you questions to select appropriate options. There are 4 main qustions.

  • What library to use?
  • Do you want to create new tags that aren’t already present?
  • Do you want to restore the default tags to images that are not present in your target library
  • If you already have a tag image, do you want to overwrite it?

Once you answer these questions, the script will run.

Extending The Library

The library works on a the same JSON export as stash. All you need to do is submit a PR with your JSON files from the zip created upon export from the tagger.

Extract the zip file into a new subfolder in the Library directory named for your tag image collection. DM me on Discord and I can merge it into the main.

Public mirorr of the extracted tags: StashTagSkins with GitHub - feederbox826/StashTagSkinsExtractor: Extractor for https://github.com/Stash-KennyG/StashTagSkins

unfortunately no gallery/browse view

Every single one of those files is a json 64 encoded as extracted by the app. Here is a ChatGPT generated function.


function Convert-JsonBase64Image {
    [CmdletBinding()]
    param(
        # Path to the JSON file
        [Parameter(Mandatory)]
        [string]$Path,

        # Optional property path to the base64 string inside the JSON
        # Use slash-separated path for nested objects, for example: "image/data" or "icon/base64"
        [string]$Key,

        # Output file path. If omitted, the function picks a name from the JSON file name and inferred extension.
        [string]$OutputPath
    )

    # Helpers
    function Get-NestedValue($obj, $path) {
        $cur = $obj
        foreach ($part in $path -split '/') {
            if (-not $part) { continue }
            if ($cur -is [System.Collections.IDictionary]) {
                if ($cur.Contains($part)) { $cur = $cur[$part] } else { return $null }
            } else {
                $cur = $cur | Select-Object -ExpandProperty $part -ErrorAction SilentlyContinue
            }
        }
        return $cur
    }

    function Split-DataUri($text) {
        # Returns @{ Mime="image/png"; Base64="..." } or $null
        if ($text -isnot [string]) { return $null }
        if ($text -match '^data:(?<mime>[^;]+);base64,(?<data>[A-Za-z0-9+/=]+)$') {
            return @{ Mime = $Matches['mime']; Base64 = $Matches['data'] }
        }
        return $null
    }

    function Infer-Ext($mime) {
        switch -Regex ($mime) {
            '^image/png$'      { 'png'; break }
            '^image/jpeg$'     { 'jpg'; break }
            '^image/webp$'     { 'webp'; break }
            '^image/gif$'      { 'gif'; break }
            '^image/svg\+xml$' { 'svg'; break }
            default            { 'bin' }
        }
    }

    # Load JSON
    if (-not (Test-Path -LiteralPath $Path)) {
        throw "File not found: $Path"
    }
    $raw = Get-Content -LiteralPath $Path -Raw
    $json = $raw | ConvertFrom-Json -Depth 100

    # Find the base64 text
    $b64 = $null
    $mime = $null

    if ($Key) {
        $val = Get-NestedValue -obj $json -path $Key
        if (-not $val) { throw "Key '$Key' not found in JSON." }
        $info = Split-DataUri $val
        if ($info) { $mime = $info.Mime; $b64 = $info.Base64 }
        elseif ($val -is [string]) { $b64 = $val }
        else { throw "Key '$Key' is not a string or data URI." }
    }
    else {
        # Auto-detect: look for any string property that looks like data URI or long base64
        $candidates = @()

        function Walk($node) {
            switch ($node) {
                { $_ -is [string] } {
                    $candidates += ,$node
                }
                { $_ -is [System.Collections.IEnumerable] -and $_ -isnot [string] } {
                    foreach ($n in $node) { Walk $n }
                }
                default {
                    $props = $_ | Get-Member -MemberType NoteProperty,AliasProperty -ErrorAction SilentlyContinue
                    foreach ($p in $props) {
                        $v = $_.$($p.Name)
                        Walk $v
                    }
                }
            }
        }
        Walk $json

        # Prefer data URIs, otherwise long base64-looking strings
        $dataUri = $candidates | Where-Object { $_ -match '^data:[^;]+;base64,[A-Za-z0-9+/=]+$' }
        if ($dataUri) {
            $best = $dataUri | Select-Object -First 1
            $info = Split-DataUri $best
            $mime = $info.Mime
            $b64 = $info.Base64
        } else {
            $maybeB64 = $candidates | Where-Object {
                $_.Length -gt 200 -and $_ -match '^[A-Za-z0-9+/=\r\n]+$'
            }
            if ($maybeB64) {
                $b64 = ($maybeB64 | Select-Object -First 1) -replace '\s',''
            }
        }
        if (-not $b64) { throw "Could not auto-detect a base64 or data URI string in the JSON." }
    }

    # If still a data URI
    if (-not $mime) {
        $info = Split-DataUri $b64
        if ($info) { $mime = $info.Mime; $b64 = $info.Base64 }
    }

    # Decode
    try {
        $bytes = [Convert]::FromBase64String($b64 -replace '\s','')
    } catch {
        throw "Failed to decode base64. Make sure you passed the image field, not the whole JSON."
    }

    # Pick output path
    if (-not $OutputPath) {
        $base = [System.IO.Path]::GetFileNameWithoutExtension($Path)
        $ext  = if ($mime) { '.' + (Infer-Ext $mime) } else { '.png' }
        $OutputPath = Join-Path (Split-Path -Parent $Path) ($base + $ext)
    }

    # Write file
    [IO.File]::WriteAllBytes($OutputPath, $bytes) | Out-Null
    Get-Item -LiteralPath $OutputPath
}

From there.
Convert-JsonBase64Image -Path ‘C:\temp\8K.json’