stash icon indicating copy to clipboard operation
stash copied to clipboard

[Feature] Support audio files/stories

Open binarygeek119 opened this issue 3 years ago • 23 comments

Is your feature request related to a problem? Please describe. There isn't a native way to play audio files in Stash.

Describe the solution you'd like Adding support for audio files/stories. Metadata I would like to see:

name
author
Performer
url for source
website of source
date
language 
cover front
cover back
Rating
Tags
Details

Additional context Added $40 bounty.

Related #11

binarygeek119 avatar Apr 02 '21 04:04 binarygeek119

$50 bounty added (txn #793013). Total bounty $90.

WithoutPants avatar Mar 12 '24 00:03 WithoutPants

Please. Publish this update which is of paramount importance to stash!

miltuss avatar May 04 '24 12:05 miltuss

I'd also love this. I have a huge audio library that I'd like to use here, and I have a couple additional thoughts on how it should be implemented. Edit: I've added some ideas since I posted this, and will probably add more as I learn more about how to use Stash in general and have time to think about the ideal way to implement audio.

Multiple Source/website URL entries. GonewildAudio, for example, has a reddit link and potentially several other sources. It would be good to be able to add all the links in a post, possibly by scraper eventually, so that if you need to go back and re-source the file or something, you can get it even if one of the sources is down or hidden.)

Grouping Audios. Sometimes audios come as parts of a whole, so it would be nice to tag them as such, similar to how Movies are groups of Scenes. I would propose calling it "Series", but the name doesn't matter much.

Markers. Same as in videos, it would be helpful to have these to jump back to your favorite parts.

Playback of video files as audio. There are some creators who post the original/best/only source to video platforms like pornhub. The "videos" are still audio content, usually with some cover art as the "video". It would be ideal to allow a toggle on a per-item or per-file basis that switches on/off video playback for a video file in an audio library, in the rare case in which the video track is anything other than stationary cover art, so that we don't waste bandwidth playing back video unnecessarily.

styxmaster06 avatar Jun 05 '24 18:06 styxmaster06

This would be extremely helpful in organizing audio collections from r/GoneWildAudio. Why not implement the simplest support for audio files to be played as Audios? The rest would be identical to scenes with some minor differences.

Hydronicx avatar Jun 15 '24 20:06 Hydronicx

If anyone is interested I've made a temporary solution:

  1. Add audio extensions in the library settings.
  2. Create an "Audio" tag.
  3. Make and "Audio" directory to put the files in.
  4. Use Auto Tag to tag the files inside the folder.
  5. Use custom css and js to modify or replace the player (like below).

Here are my custom css and js: css:

/* Audio: Hide player. */
.VideoPlayer.audio .vjs-big-play-button, .VideoPlayer.audio .scrubber-wrapper, .VideoPlayer.audio .vjs-fullscreen-control,
.VideoPlayer.audio .vjs-control-bar {
  display: none !important;
}

js:


// Replace player
function setAudioElement(bool){
  const pl = document.querySelector(".VideoPlayer")
  var elm = document.getElementById('VideoJsPlayer_html5_api');
  if (bool){
    const videoElement = elm
    pl.classList.add("audio")
    var audioElement = document.createElement('audio');
    for (var i = 0; i < videoElement.attributes.length; i++) {
        audioElement.setAttribute(videoElement.attributes[i].name, videoElement.attributes[i].value);
    }
    audioElement.src = videoElement.src
    audioElement.controls = true
    audioElement.autoplay = false
    videoElement.parentNode.replaceChild(audioElement, videoElement);
    audioElement.parentNode.insertBefore(document.querySelector(".vjs-poster"), audioElement);
  }else{
    const audioElement = elm
    pl.classList.remove("audio")
    var videoElement = document.createElement('video');
    for (var i = 0; i < audioElement.attributes.length; i++) {
      videoElement.setAttribute(audioElement.attributes[i].name, audioElement.attributes[i].value);
    }
    videoElement.src = audioElement.src
    videoElement.controls = false
    audioElement.parentNode.replaceChild(videoElement, audioElement);
    videoElement.parentNode.insertBefore(videoElement, document.querySelector(".vjs-poster"));
  }
}


// Add "Audio Only" switch button
function audioswitch(){
  const elm = document.querySelector(".scene-toolbar-group")
  const btng = document.createElement("div")
  const btn = document.createElement("button")
  const pl = document.querySelector(".VideoPlayer")
  if (pl.classList.contains("audio")) {
    btn.style.background = "rgba(138,155,168,.15)"
  }
  btn.classList.add("minimal", "btn", "btn-secondary")
  btn.innerHTML= "Audio Only"
  btn.id = "audioSwitch"
  btn.onclick = function (){
    const pl = document.querySelector(".VideoPlayer")
    const bt = document.getElementById("audioSwitch")
    if (pl.classList.contains("audio")){
      setAudioElement(false);
      bt.style.background = "transparent"
    } else {
      setAudioElement(true);
      bt.style.background = "rgba(138,155,168,.15)"
    }
  }
  btng.classList.add("btn-group")
  btng.appendChild(btn)
  elm.appendChild(btng)
}


// Find audio tag and replace player
function findAudioTag() {
  const links = document.querySelectorAll('.tag-item a');
if(links){
  links.forEach(link => {
      const divText = link.querySelector('div').textContent.trim();

      if (divText === "Audio") {
        setAudioElement(true)
      }
  });}
}

// Check if page is a scene page
function shouldRunOnPage() {
    const currentPath = window.location.pathname;
    const itemsPagePattern = /^\/scenes\/\d+(.*)/;
    return itemsPagePattern.test(currentPath);
}

// Check if elements are rendered every 100ms then run the scripts
if (shouldRunOnPage()){
function checkRendered() {
  const targetElement2 = document.querySelector('.VideoPlayer');
  const targetElement3 = document.querySelector('#VideoJsPlayer_html5_api')?.src
  const targetElement4 = document.querySelector('.scene-toolbar-group')
  if (targetElement2 && targetElement3 && targetElement4) {
    findAudioTag()
    audioswitch()
    clearInterval(intervalId);
  }
}

    const intervalId = setInterval(checkRendered, 100);
}

d0t-d0t-d0t avatar Aug 15 '24 08:08 d0t-d0t-d0t

Here are my custom css and js:

@d0t-d0t-d0t I don't know what this code is supposed to do, but it doesn't work with stash version 0.27. No video/audio player replacement has been observed on audio files with the Audio tag. Can you help resolve this issue?

miltuss avatar Sep 23 '24 09:09 miltuss

Here are my custom css and js:

@d0t-d0t-d0t I don't know what this code is supposed to do, but it doesn't work with stash version 0.27. No video/audio player replacement has been observed on audio files with the Audio tag. Can you help resolve this issue?

You should try refreshing the page, the code isn't perfect.

This is the updated code:

function setAudioElement(bool){
  const pl = document.querySelector(".VideoPlayer")
  var elm = document.getElementById('VideoJsPlayer_html5_api');
  if (bool){
    const videoElement = elm
    pl.classList.add("audio")
    var audioElement = document.createElement('audio');
    for (var i = 0; i < videoElement.attributes.length; i++) {
        audioElement.setAttribute(videoElement.attributes[i].name, videoElement.attributes[i].value);
    }
    audioElement.src = videoElement.src.replace(/(^.*\/scene\/\d+\/)(stream.*)\?/, "$1stream?")
    audioElement.controls = true
    audioElement.autoplay = false
    videoElement.parentNode.replaceChild(audioElement, videoElement);
    audioElement.parentNode.insertBefore(document.querySelector(".vjs-poster"), audioElement);
  }else{
    const audioElement = elm
    pl.classList.remove("audio")
    var videoElement = document.createElement('video');
    for (var i = 0; i < audioElement.attributes.length; i++) {
      videoElement.setAttribute(audioElement.attributes[i].name, audioElement.attributes[i].value);
    }
    videoElement.src = audioElement.src
    videoElement.controls = false
    audioElement.parentNode.replaceChild(videoElement, audioElement);
    videoElement.parentNode.insertBefore(videoElement, document.querySelector(".vjs-poster"));
  }
}



function audioswitch(){
  const elm = document.querySelector(".scene-toolbar-group")
  const btng = document.createElement("div")
  const btn = document.createElement("button")
  const pl = document.querySelector(".VideoPlayer")
  if (pl.classList.contains("audio")) {
    btn.style.background = "rgba(138,155,168,.15)"
  }
  btn.classList.add("minimal", "btn", "btn-secondary")
  btn.innerHTML= "Audio Only"
  btn.id = "audioSwitch"
  btn.onclick = function (){
    const pl = document.querySelector(".VideoPlayer")
    const bt = document.getElementById("audioSwitch")
    if (pl.classList.contains("audio")){
      setAudioElement(false);
      bt.style.background = "transparent"
    } else {
      setAudioElement(true);
      bt.style.background = "rgba(138,155,168,.15)"
    }
  }
  btng.classList.add("btn-group")
  btng.appendChild(btn)
  elm.appendChild(btng)
}



function findAudioTag() {
  // Select all <a> elements within the <span>
  const links = document.querySelectorAll('.tag-item a');
if(links){
  // Iterate through each <a> element
  links.forEach(link => {
      // Get the text content of the <div> inside the <a>
      const divText = link.querySelector('div').textContent.trim();

      // Check if the text matches the target text
      if (divText === "Audio") {
        setAudioElement(true)
      }
  });}
}

function shouldRunOnPage() {
    const currentPath = window.location.pathname;
    const itemsPagePattern = /^\/scenes\/\d+(.*)/;
    return itemsPagePattern.test(currentPath);
}

if (shouldRunOnPage()){
function checkRendered() {
  const targetElement2 = document.querySelector('.VideoPlayer');
  const targetElement3 = document.querySelector('#VideoJsPlayer_html5_api')?.src
  const targetElement4 = document.querySelector('.scene-toolbar-group')
  if (targetElement2 && targetElement3 && targetElement4) {
    findAudioTag()
    audioswitch()
    // Stop polling
    clearInterval(intervalId);
  }
}


    const intervalId = setInterval(checkRendered, 100);
}

Remember to use this custom css:

/* Audio: Remove big play button (leave only the button in controls). */
.VideoPlayer.audio .vjs-big-play-button, .VideoPlayer.audio .scrubber-wrapper, .VideoPlayer.audio .vjs-fullscreen-control,
.VideoPlayer.audio .vjs-control-bar {
  display: none !important;
}

/* Audio: Make the controlbar visible by default */
.VideoPlayer.audio .vjs-control-bar {
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
}
/* Make player height minimum to the controls height so when we hide video/poster area the controls are displayed correctly. */
/*
.VideoPlayer.audio .video-js {
  height: fill !important;
}

.VideoPlayer.audio .video-wrapper {
  height: 6rem !important;
}
*/

d0t-d0t-d0t avatar Sep 23 '24 11:09 d0t-d0t-d0t

@d0t-d0t-d0t I updated the code, and refreshed the page several times, I even cleared the browser cache and restarted the docker container several times. CCSS and custom javascript are activated in the settings but the code changes absolutely nothing to the reader!

miltuss avatar Sep 23 '24 13:09 miltuss

@miltuss You should see something like this: image-1

There should also be an 'Audio Only' toggle button

d0t-d0t-d0t avatar Sep 23 '24 16:09 d0t-d0t-d0t

@d0t-d0t-d0t Unfortunately your script only works for you

miltuss avatar Sep 25 '24 20:09 miltuss

@miltuss That's strange, you could try to copy the script and paste it in the browser console to see if it works, i was thinking of making a plugin (i don't know if plugins can use js to modify preexisting elements) if i had the time.

I aslo converted it in a simple user script:

// ==UserScript==
// @name         Stash Audio Switch
// @version      0.1
// @description  Add an audio-only switch to video player and apply custom CSS
// @match        https://example.com/scenes/*
// @run-at       document-idle
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        .VideoPlayer.audio .vjs-big-play-button, 
        .VideoPlayer.audio .scrubber-wrapper, 
        .VideoPlayer.audio .vjs-fullscreen-control,
        .VideoPlayer.audio .vjs-control-bar {
            display: none !important;
        }
    `);

    function setAudioElement(bool) {
        const pl = document.querySelector(".VideoPlayer");
        var elm = document.getElementById('VideoJsPlayer_html5_api');
        if (bool) {
            const videoElement = elm;
            pl.classList.add("audio");
            var audioElement = document.createElement('audio');
            for (var i = 0; i < videoElement.attributes.length; i++) {
                audioElement.setAttribute(videoElement.attributes[i].name, videoElement.attributes[i].value);
            }
            audioElement.src = videoElement.src.replace(/(^.*\/scene\/\d+\/)(stream.*)\?/, "$1stream?");
            audioElement.controls = true;
            audioElement.autoplay = false;
            videoElement.parentNode.replaceChild(audioElement, videoElement);
            audioElement.parentNode.insertBefore(document.querySelector(".vjs-poster"), audioElement);
        } else {
            const audioElement = elm;
            pl.classList.remove("audio");
            var videoElement = document.createElement('video');
            for (var i = 0; i < audioElement.attributes.length; i++) {
                videoElement.setAttribute(audioElement.attributes[i].name, audioElement.attributes[i].value);
            }
            videoElement.src = audioElement.src;
            videoElement.controls = false;
            audioElement.parentNode.replaceChild(videoElement, audioElement);
            videoElement.parentNode.insertBefore(videoElement, document.querySelector(".vjs-poster"));
        }
    }

    function audioswitch() {
        const elm = document.querySelector(".scene-toolbar-group");
        const btng = document.createElement("div");
        const btn = document.createElement("button");
        const pl = document.querySelector(".VideoPlayer");
        if (pl.classList.contains("audio")) {
            btn.style.background = "rgba(138,155,168,.15)";
        }
        btn.classList.add("minimal", "btn", "btn-secondary");
        btn.innerHTML = "Audio Only";
        btn.id = "audioSwitch";
        btn.onclick = function() {
            const pl = document.querySelector(".VideoPlayer");
            const bt = document.getElementById("audioSwitch");
            if (pl.classList.contains("audio")) {
                setAudioElement(false);
                bt.style.background = "transparent";
            } else {
                setAudioElement(true);
                bt.style.background = "rgba(138,155,168,.15)";
            }
        };
        btng.classList.add("btn-group");
        btng.appendChild(btn);
        elm.appendChild(btng);
    }

    function findAudioTag() {
        const links = document.querySelectorAll('.tag-item a');
        if (links) {
            links.forEach(link => {
                const divText = link.querySelector('div').textContent.trim();
                if (divText === "Audio") {
                    setAudioElement(true);
                }
            });
        }
    }

    function checkRendered() {
        const targetElement2 = document.querySelector('.VideoPlayer');
        const targetElement3 = document.querySelector('#VideoJsPlayer_html5_api')?.src;
        const targetElement4 = document.querySelector('.scene-toolbar-group');
        if (targetElement2 && targetElement3 && targetElement4) {
            findAudioTag();
            audioswitch();
            clearInterval(intervalId);
        }
    }

    const intervalId = setInterval(checkRendered, 100);
})();

d0t-d0t-d0t avatar Sep 25 '24 22:09 d0t-d0t-d0t

@d0t-d0t-d0t I figured out what's wrong with the code, when we navigate to stash to open an audio file with the Audio Tag, the script doesn't run like this:

Sélection_560

The script necessarily needs a refresh of the reader page to run.

Sélection_561

In addition, markers do not work with the browser's built-in reader. So the method consisting of using the Stash player with the HLS transcoder remains the best while waiting for real integration of audio support in Stash.

miltuss avatar Sep 26 '24 12:09 miltuss

@miltuss

I figured out what's wrong with the code, when we navigate to stash to open an audio file with the Audio Tag

I forgot to mention that you have to replace the match with the url of your stash instance.

In addition, markers do not work with the browser's built-in reader.

The code can be modified to use the stash player but i haven't tested it with audio files.

d0t-d0t-d0t avatar Sep 26 '24 15:09 d0t-d0t-d0t

@d0t-d0t-d0t

I forgot to mention that you have to replace the match with the url of your stash instance.

I had inserted the URL of my instance instead of the default URL of the userscript from the first test

The code can be modified to use the stash player but i haven't tested it with audio files.

The player must read the audio correctly provided the HLS codec is selected. I don't know if it is possible to code a script that can automatically select the HSL on stash's Video.JS player when playing an audio file. I think this would be the best DIY to do while waiting for the official release of this feature!

miltuss avatar Sep 26 '24 22:09 miltuss

I have created the audio-transcodes plugin for stash. This plugin converts an audio file to a video and stash will play this video instead allowing the normal video player to work. Install the plugin and run tasks > audio-transcodes > Process All

Tweeticoats avatar Sep 27 '24 01:09 Tweeticoats

By doing this, your audio files will not be recognized by stash as audio tracks when integrating the AudioStoris functionality into Stash.

HLS allows you to play audio files in Stash, you just need to find a way to exclude other formats by default when playing an audio file, so that playback starts immediately on HLS and not manually switch to HLS.

miltuss avatar Sep 27 '24 05:09 miltuss

Here are the updated script/css and userscript that use the original player and automatically change the transcoder to hls: https://gist.github.com/d0t-d0t-d0t/d061b10f12587a2d4e50c392d319a2f5

d0t-d0t-d0t avatar Sep 28 '24 19:09 d0t-d0t-d0t

This would be better suited for a gist. Issue comments are not a good place for versioned code and take up an immense amount of vertical space

feederbox826 avatar Sep 29 '24 06:09 feederbox826

This would be better suited for a gist.

I didn't know about gits, I've updated the comment.

d0t-d0t-d0t avatar Sep 29 '24 09:09 d0t-d0t-d0t

I've made it in to a plugin: https://github.com/d0t-d0t-d0t/StashAudioPlayer

d0t-d0t-d0t avatar Sep 29 '24 12:09 d0t-d0t-d0t