mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-20 11:12:04 +00:00
subtitle parsing progress, v 0.99999
This commit is contained in:
parent
792021bcb4
commit
7ef4f14de5
6 changed files with 379 additions and 360 deletions
|
|
@ -207,11 +207,13 @@ function viewAnime(media) {
|
|||
document.querySelector(".view .banner img").src = ""
|
||||
document.querySelector(".view .banner img").src = media.bannerImage
|
||||
document.querySelector(".view .contain-img").src = media.coverImage.extraLarge
|
||||
document.querySelector(".view .title").textContent = !!media.title.english ? media.title.english : media.title.romaji
|
||||
document.querySelector(".view .desc").innerHTML = !!media.description ? media.description : ""
|
||||
document.querySelector(".view .title").textContent = media.title.english || media.title.romaji
|
||||
document.querySelector(".view .desc").innerHTML = media.description || ""
|
||||
document.querySelector(".view .details").innerHTML = ""
|
||||
document.querySelector(".view #play").onclick = function () { nyaaSearch(media, document.querySelector(".view #ep").value); halfmoon.toggleModal("view") }
|
||||
detailsCreator(media)
|
||||
document.querySelector(".view #ep").value = 1
|
||||
document.querySelector(".view #ep").max = media.episodes || 999
|
||||
document.querySelector(".view .details").appendChild(detailsfrag)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
<div class="row">
|
||||
<div class="col-md-3 mt-nc pb-20 d-flex flex-column justify-content-between">
|
||||
<img class="contain-img rounded w-full">
|
||||
<div class="input-group">
|
||||
<div class="input-group pt-10">
|
||||
<div class="input-group-prepend">
|
||||
<button class="btn bg-primary pr-5 pl-10" type="button" id="play">Play
|
||||
Episode</button>
|
||||
|
|
@ -216,10 +216,8 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="gallery">
|
||||
<div class="gallery h-full overflow-y-scroll">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
468
app/bundle.js
468
app/bundle.js
|
|
@ -13069,245 +13069,251 @@ if (typeof Object.create === 'function') {
|
|||
},{}],14:[function(require,module,exports){
|
||||
(function (Buffer){
|
||||
const Transform = require('readable-stream').Transform
|
||||
const ebml = require('ebml')
|
||||
const ebmlBlock = require('ebml-block')
|
||||
const readElement = require('./lib/read-element')
|
||||
|
||||
// track elements we care about
|
||||
const TRACK_ELEMENTS = ['TrackNumber', 'TrackType', 'Language', 'CodecID', 'CodecPrivate']
|
||||
const SUBTITLE_TYPES = ['S_TEXT/UTF8', 'S_TEXT/SSA', 'S_TEXT/ASS']
|
||||
const ASS_KEYS = ['readOrder', 'layer', 'style', 'name', 'marginL', 'marginR', 'marginV', 'effect', 'text']
|
||||
|
||||
const CUES_ID = Buffer.from('1C53BB6B', 'hex')
|
||||
|
||||
class MatroskaSubtitles extends Transform {
|
||||
constructor ({ prevInstance, offset } = {}) {
|
||||
super()
|
||||
|
||||
let currentTrack = null
|
||||
let currentSubtitleBlock = null
|
||||
let currentClusterTimecode = null
|
||||
|
||||
let currentSeekID = null
|
||||
|
||||
let waitForNext = false
|
||||
|
||||
this.decoder = new ebml.Decoder()
|
||||
|
||||
if (prevInstance instanceof MatroskaSubtitles) {
|
||||
if (offset == null) throw new Error('no offset')
|
||||
|
||||
prevInstance.once('drain', () => {
|
||||
// prevInstance.end()
|
||||
console.log('prevInstance drained')
|
||||
})
|
||||
|
||||
if (offset === 0) {
|
||||
// just begin normal parsing
|
||||
this.subtitleTracks = prevInstance.subtitleTracks || new Map()
|
||||
this.timecodeScale = prevInstance.timecodeScale || 1
|
||||
this.cues = prevInstance.cues
|
||||
|
||||
this.decoder.on('data', _onMetaData.bind(this))
|
||||
return
|
||||
}
|
||||
|
||||
// copy previous metadata
|
||||
this.subtitleTracks = prevInstance.subtitleTracks
|
||||
this.timecodeScale = prevInstance.timecodeScale
|
||||
const ebml = require('ebml')
|
||||
const ebmlBlock = require('ebml-block')
|
||||
const readElement = require('./lib/read-element')
|
||||
|
||||
// track elements we care about
|
||||
const TRACK_ELEMENTS = ['TrackNumber', 'TrackType', 'Language', 'CodecID', 'CodecPrivate']
|
||||
const SUBTITLE_TYPES = ['S_TEXT/UTF8', 'S_TEXT/SSA', 'S_TEXT/ASS']
|
||||
const ASS_KEYS = ['readOrder', 'layer', 'style', 'name', 'marginL', 'marginR', 'marginV', 'effect', 'text']
|
||||
|
||||
const CUES_ID = Buffer.from('1C53BB6B', 'hex')
|
||||
|
||||
class MatroskaSubtitles extends Transform {
|
||||
constructor ({ prevInstance, offset } = {}) {
|
||||
super()
|
||||
|
||||
let currentTrack = null
|
||||
let currentSubtitleBlock = null
|
||||
let currentClusterTimecode = null
|
||||
|
||||
let currentSeekID = null
|
||||
|
||||
let waitForNext = false
|
||||
|
||||
this.decoder = new ebml.Decoder()
|
||||
|
||||
if (prevInstance instanceof MatroskaSubtitles) {
|
||||
if (offset == null) throw new Error('no offset')
|
||||
|
||||
prevInstance.once('drain', () => {
|
||||
// prevInstance.end()
|
||||
console.log('prevInstance drained')
|
||||
})
|
||||
|
||||
if (offset === 0) {
|
||||
// just begin normal parsing
|
||||
this.subtitleTracks = prevInstance.subtitleTracks || new Map()
|
||||
this.timecodeScale = prevInstance.timecodeScale || 1
|
||||
this.cues = prevInstance.cues
|
||||
|
||||
if (!this.cues) {
|
||||
this.decoder = null
|
||||
return console.warn('No cues was parsed. Subtitle parsing disabled.')
|
||||
}
|
||||
|
||||
// find a cue that's close to the file offset
|
||||
// const cueArray = Uint32Array.from(this.cues.positions)
|
||||
// cueArray.sort()
|
||||
const cueArray = Array.from(this.cues.positions)
|
||||
cueArray.sort((a, b) => a - b)
|
||||
|
||||
const closestCue = cueArray.find(i => i >= offset)
|
||||
|
||||
if (closestCue != null) {
|
||||
// prepare to skip file stream until we hit a cue position
|
||||
this.skip = closestCue - offset
|
||||
// set internal decoder position to output consistent file offsets
|
||||
this.decoder.total = closestCue
|
||||
|
||||
// console.log('using cue:', closestCue)
|
||||
|
||||
this.decoder.on('data', _onMetaData.bind(this))
|
||||
} else {
|
||||
this.decoder = null
|
||||
console.warn(`No cues for offset ${offset}. Subtitle parsing disabled.`)
|
||||
}
|
||||
} else {
|
||||
if (offset) {
|
||||
this.decoder = null
|
||||
console.error(`Offset is ${offset}, and must be 0 for initial instance. Subtitle parsing disabled.`)
|
||||
return
|
||||
}
|
||||
|
||||
this.subtitleTracks = new Map()
|
||||
this.timecodeScale = 1
|
||||
|
||||
|
||||
this.decoder.on('data', _onMetaData.bind(this))
|
||||
return
|
||||
}
|
||||
|
||||
function _onMetaData (chunk) {
|
||||
if (waitForNext) {
|
||||
waitForNext = false
|
||||
// Keep cues if this is the same segment
|
||||
if (!this.cues || this.cues.start !== chunk[1].start) {
|
||||
this.cues = { start: chunk[1].start, positions: new Set() }
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk[0] === 'start' && chunk[1].name === 'Segment') {
|
||||
// TODO: only record first segment?
|
||||
// TODO: find a simpler way to do this
|
||||
waitForNext = true
|
||||
}
|
||||
|
||||
if (chunk[1].name === 'SeekID') {
|
||||
// TODO: .value is undefined for some reason?
|
||||
currentSeekID = chunk[1].data
|
||||
}
|
||||
|
||||
if (currentSeekID && chunk[1].name === 'SeekPosition') {
|
||||
if (CUES_ID.equals(currentSeekID)) {
|
||||
// hack: this is not a cue position, but the position to the cue data itself,
|
||||
// in case it's not located at the beginning of the file.
|
||||
this.cues.positions.add(this.cues.start + chunk[1].value)
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk[1].name === 'CueClusterPosition') {
|
||||
this.cues.positions.add(this.cues.start + chunk[1].value)
|
||||
}
|
||||
|
||||
if (chunk[0] === 'end' && chunk[1].name === 'Cues') {
|
||||
this.emit('cues')
|
||||
}
|
||||
|
||||
// Segment Information
|
||||
if (chunk[1].name === 'TimecodeScale') {
|
||||
this.timecodeScale = readElement(chunk[1]) / 1000000
|
||||
}
|
||||
|
||||
// Tracks
|
||||
if (chunk[0] === 'start' && chunk[1].name === 'TrackEntry') {
|
||||
currentTrack = {}
|
||||
}
|
||||
|
||||
if (currentTrack && chunk[0] === 'tag') {
|
||||
// save info about track currently being scanned
|
||||
if (TRACK_ELEMENTS.includes(chunk[1].name)) {
|
||||
currentTrack[chunk[1].name] = readElement(chunk[1])
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk[0] === 'end' && chunk[1].name === 'TrackEntry') {
|
||||
if (currentTrack.TrackType === 0x11) { // Subtitle Track
|
||||
if (SUBTITLE_TYPES.includes(currentTrack.CodecID)) {
|
||||
const track = {
|
||||
number: currentTrack.TrackNumber,
|
||||
language: currentTrack.Language,
|
||||
type: currentTrack.CodecID.substring(7).toLowerCase()
|
||||
}
|
||||
|
||||
if (currentTrack.CodecPrivate) {
|
||||
// only SSA/ASS
|
||||
track.header = currentTrack.CodecPrivate.toString('utf8')
|
||||
}
|
||||
|
||||
this.subtitleTracks.set(currentTrack.TrackNumber, track)
|
||||
}
|
||||
}
|
||||
currentTrack = null
|
||||
}
|
||||
|
||||
if (chunk[0] === 'end' && chunk[1].name === 'Tracks') {
|
||||
// this.decoder.removeListener('data', _onMetaData)
|
||||
|
||||
// if (this.subtitleTracks.size <= 0) return this.end()
|
||||
|
||||
// this.decoder.on('data', _onClusterData)
|
||||
this.emit('tracks', Array.from(this.subtitleTracks.values()))
|
||||
}
|
||||
// }
|
||||
|
||||
// function _onClusterData (chunk) {
|
||||
// TODO: assuming this is a Cluster `Timecode`
|
||||
if (chunk[1].name === 'Timecode') {
|
||||
currentClusterTimecode = readElement(chunk[1])
|
||||
}
|
||||
|
||||
if (chunk[1].name === 'Block') {
|
||||
const block = ebmlBlock(chunk[1].data)
|
||||
|
||||
if (this.subtitleTracks.has(block.trackNumber)) {
|
||||
const type = this.subtitleTracks.get(block.trackNumber).type
|
||||
|
||||
const subtitle = {
|
||||
text: block.frames[0].toString('utf8'),
|
||||
time: (block.timecode + currentClusterTimecode) * this.timecodeScale
|
||||
}
|
||||
|
||||
if (type === 'ass' || type === 'ssa') {
|
||||
// extract SSA/ASS keys
|
||||
const values = subtitle.text.split(',')
|
||||
// ignore read-order, and skip layer if ssa
|
||||
let i = type === 'ssa' ? 2 : 1
|
||||
for (; i < 9; i++) {
|
||||
subtitle[ASS_KEYS[i]] = values[i]
|
||||
}
|
||||
// re-append extra text that might have been split
|
||||
for (i = 9; i < values.length; i++) {
|
||||
subtitle.text += ',' + values[i]
|
||||
}
|
||||
}
|
||||
|
||||
currentSubtitleBlock = [subtitle, block.trackNumber]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: assuming `BlockDuration` exists and always comes after `Block`
|
||||
if (currentSubtitleBlock && chunk[1].name === 'BlockDuration') {
|
||||
currentSubtitleBlock[0].duration = readElement(chunk[1]) * this.timecodeScale
|
||||
|
||||
this.emit('subtitle', ...currentSubtitleBlock)
|
||||
|
||||
currentSubtitleBlock = null
|
||||
}
|
||||
|
||||
// copy previous metadata
|
||||
this.subtitleTracks = prevInstance.subtitleTracks
|
||||
this.timecodeScale = prevInstance.timecodeScale
|
||||
this.cues = prevInstance.cues
|
||||
|
||||
if (!this.cues) {
|
||||
this.decoder = null
|
||||
return console.warn('No cues was parsed. Subtitle parsing disabled.')
|
||||
}
|
||||
}
|
||||
|
||||
_transform (chunk, _, callback) {
|
||||
if (!this.decoder) return callback(null, chunk)
|
||||
|
||||
if (this.skip) {
|
||||
// skip bytes to reach cue position
|
||||
if (this.skip < chunk.length) {
|
||||
// slice chunk
|
||||
const sc = chunk.slice(this.skip)
|
||||
this.skip = 0
|
||||
this.decoder.write(sc)
|
||||
} else {
|
||||
// skip entire chunk
|
||||
this.skip -= chunk.length
|
||||
}
|
||||
|
||||
// find a cue that's close to the file offset
|
||||
// const cueArray = Uint32Array.from(this.cues.positions)
|
||||
// cueArray.sort()
|
||||
const cueArray = Array.from(this.cues.positions)
|
||||
cueArray.sort((a, b) => a - b)
|
||||
|
||||
const closestCue = cueArray.find(i => i >= offset)
|
||||
|
||||
if (closestCue != null) {
|
||||
// prepare to skip file stream until we hit a cue position
|
||||
this.skip = closestCue - offset
|
||||
// set internal decoder position to output consistent file offsets
|
||||
this.decoder.total = closestCue
|
||||
|
||||
// console.log('using cue:', closestCue)
|
||||
|
||||
this.decoder.on('data', _onMetaData.bind(this))
|
||||
} else {
|
||||
this.decoder.write(chunk)
|
||||
this.decoder = null
|
||||
console.warn(`No cues for offset ${offset}. Subtitle parsing disabled.`)
|
||||
}
|
||||
} else {
|
||||
if (offset) {
|
||||
this.decoder = null
|
||||
console.error(`Offset is ${offset}, and must be 0 for initial instance. Subtitle parsing disabled.`)
|
||||
return
|
||||
}
|
||||
|
||||
this.subtitleTracks = new Map()
|
||||
this.timecodeScale = 1
|
||||
|
||||
this.decoder.on('data', _onMetaData.bind(this))
|
||||
}
|
||||
|
||||
function _onMetaData (chunk) {
|
||||
if (waitForNext) {
|
||||
waitForNext = false
|
||||
// Keep cues if this is the same segment
|
||||
if (!this.cues) {
|
||||
this.cues = { start: chunk[1].start, positions: new Set() }
|
||||
} else if (this.cues.start !== chunk[1].start) {
|
||||
this.cues = { start: chunk[1].start, positions: new Set() }
|
||||
console.warn('New segment found - resetting cues! Not sure we can handle this!?')
|
||||
} else {
|
||||
console.info('Saw first segment again. Keeping cues.')
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk[0] === 'start' && chunk[1].name === 'Segment') {
|
||||
// TODO: only record first segment?
|
||||
// TODO: find a simpler way to do this
|
||||
waitForNext = true
|
||||
}
|
||||
|
||||
if (chunk[1].name === 'SeekID') {
|
||||
// TODO: .value is undefined for some reason?
|
||||
currentSeekID = chunk[1].data
|
||||
}
|
||||
|
||||
if (currentSeekID && chunk[1].name === 'SeekPosition') {
|
||||
//if (CUES_ID.equals(currentSeekID)) {
|
||||
// hack: this is not a cue position, but the position to the cue data itself,
|
||||
// in case it's not located at the beginning of the file.
|
||||
// actually, just add all seek positions.
|
||||
this.cues.positions.add(this.cues.start + chunk[1].value)
|
||||
//}
|
||||
}
|
||||
|
||||
if (chunk[1].name === 'CueClusterPosition') {
|
||||
this.cues.positions.add(this.cues.start + chunk[1].value)
|
||||
}
|
||||
|
||||
if (chunk[0] === 'end' && chunk[1].name === 'Cues') {
|
||||
this.emit('cues')
|
||||
}
|
||||
|
||||
// Segment Information
|
||||
if (chunk[1].name === 'TimecodeScale') {
|
||||
this.timecodeScale = readElement(chunk[1]) / 1000000
|
||||
}
|
||||
|
||||
// Tracks
|
||||
if (chunk[0] === 'start' && chunk[1].name === 'TrackEntry') {
|
||||
currentTrack = {}
|
||||
}
|
||||
|
||||
if (currentTrack && chunk[0] === 'tag') {
|
||||
// save info about track currently being scanned
|
||||
if (TRACK_ELEMENTS.includes(chunk[1].name)) {
|
||||
currentTrack[chunk[1].name] = readElement(chunk[1])
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk[0] === 'end' && chunk[1].name === 'TrackEntry') {
|
||||
if (currentTrack.TrackType === 0x11) { // Subtitle Track
|
||||
if (SUBTITLE_TYPES.includes(currentTrack.CodecID)) {
|
||||
const track = {
|
||||
number: currentTrack.TrackNumber,
|
||||
language: currentTrack.Language,
|
||||
type: currentTrack.CodecID.substring(7).toLowerCase()
|
||||
}
|
||||
|
||||
if (currentTrack.CodecPrivate) {
|
||||
// only SSA/ASS
|
||||
track.header = currentTrack.CodecPrivate.toString('utf8')
|
||||
}
|
||||
|
||||
this.subtitleTracks.set(currentTrack.TrackNumber, track)
|
||||
}
|
||||
}
|
||||
currentTrack = null
|
||||
}
|
||||
|
||||
if (chunk[0] === 'end' && chunk[1].name === 'Tracks') {
|
||||
// this.decoder.removeListener('data', _onMetaData)
|
||||
|
||||
// if (this.subtitleTracks.size <= 0) return this.end()
|
||||
|
||||
// this.decoder.on('data', _onClusterData)
|
||||
this.emit('tracks', Array.from(this.subtitleTracks.values()))
|
||||
}
|
||||
// }
|
||||
|
||||
// function _onClusterData (chunk) {
|
||||
// TODO: assuming this is a Cluster `Timecode`
|
||||
if (chunk[1].name === 'Timecode') {
|
||||
currentClusterTimecode = readElement(chunk[1])
|
||||
}
|
||||
|
||||
if (chunk[1].name === 'Block') {
|
||||
const block = ebmlBlock(chunk[1].data)
|
||||
|
||||
if (this.subtitleTracks.has(block.trackNumber)) {
|
||||
const type = this.subtitleTracks.get(block.trackNumber).type
|
||||
|
||||
const subtitle = {
|
||||
text: block.frames[0].toString('utf8'),
|
||||
time: (block.timecode + currentClusterTimecode) * this.timecodeScale
|
||||
}
|
||||
|
||||
if (type === 'ass' || type === 'ssa') {
|
||||
// extract SSA/ASS keys
|
||||
const values = subtitle.text.split(',')
|
||||
// ignore read-order, and skip layer if ssa
|
||||
let i = type === 'ssa' ? 2 : 1
|
||||
for (; i < 9; i++) {
|
||||
subtitle[ASS_KEYS[i]] = values[i]
|
||||
}
|
||||
// re-append extra text that might have been split
|
||||
for (i = 9; i < values.length; i++) {
|
||||
subtitle.text += ',' + values[i]
|
||||
}
|
||||
}
|
||||
|
||||
currentSubtitleBlock = [subtitle, block.trackNumber]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: assuming `BlockDuration` exists and always comes after `Block`
|
||||
if (currentSubtitleBlock && chunk[1].name === 'BlockDuration') {
|
||||
currentSubtitleBlock[0].duration = readElement(chunk[1]) * this.timecodeScale
|
||||
|
||||
this.emit('subtitle', ...currentSubtitleBlock)
|
||||
|
||||
currentSubtitleBlock = null
|
||||
}
|
||||
|
||||
callback(null, chunk)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MatroskaSubtitles
|
||||
|
||||
_transform (chunk, _, callback) {
|
||||
if (!this.decoder) return callback(null, chunk)
|
||||
|
||||
if (this.skip) {
|
||||
// skip bytes to reach cue position
|
||||
if (this.skip < chunk.length) {
|
||||
// slice chunk
|
||||
const sc = chunk.slice(this.skip)
|
||||
this.skip = 0
|
||||
this.decoder.write(sc)
|
||||
} else {
|
||||
// skip entire chunk
|
||||
this.skip -= chunk.length
|
||||
}
|
||||
} else {
|
||||
this.decoder.write(chunk)
|
||||
}
|
||||
|
||||
callback(null, chunk)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MatroskaSubtitles
|
||||
|
||||
}).call(this,require("buffer").Buffer)
|
||||
},{"./lib/read-element":15,"buffer":3,"ebml":12,"ebml-block":9,"readable-stream":31}],15:[function(require,module,exports){
|
||||
|
|
|
|||
|
|
@ -283,4 +283,5 @@ section {
|
|||
|
||||
section:target {
|
||||
display: block;
|
||||
height: 100%
|
||||
}
|
||||
|
|
@ -1,114 +1,147 @@
|
|||
let tracks = []
|
||||
let tracks = [],
|
||||
parser
|
||||
|
||||
let re_newline = /\\N/g, // replace \N with newline
|
||||
|
||||
function parseSubs(range, stream) {
|
||||
if (video.src.endsWith(".mkv")) {
|
||||
console.log('set parser', range)
|
||||
|
||||
parser = new MatroskaSubtitles({ prevInstance: parser, offset: range.start })
|
||||
|
||||
parser.once('tracks', function (pTracks) {
|
||||
console.log(pTracks)
|
||||
tracks = []
|
||||
pTracks.forEach(track => {
|
||||
tracks[track.number] = video.addTextTrack('captions', track.type, track.language || track.number)
|
||||
})
|
||||
if (video.textTracks[0]) {
|
||||
video.textTracks[0].mode = "showing"
|
||||
}
|
||||
})
|
||||
|
||||
parser.once('cues', function () {
|
||||
console.log('seeking ready')
|
||||
})
|
||||
|
||||
parser.on('subtitle', function (subtitle, trackNumber) {
|
||||
subConvt(subtitle, trackNumber)
|
||||
})
|
||||
stream.pipe(parser)
|
||||
}
|
||||
}
|
||||
const re_newline = /\\N/g, // replace \N with newline
|
||||
re_softbreak = /\\n/g, // There's no equivalent function in WebVTT.
|
||||
re_hardspace = /\\h/g, // Replace with
|
||||
re_style = /\{([^}]+)\}/; // replace style
|
||||
function subConvt(result, trackNumber) {
|
||||
let cue = new VTTCue(result.time / 1000, (result.time + result.duration) / 1000, ""),
|
||||
text = result.text;
|
||||
// Support for special characters in WebVTT.
|
||||
// For obvious reasons, the ampersand one *must* be first.
|
||||
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
let style, tagsToClose = []; // Places to stash style info.
|
||||
// Subtitles may contain any number of override tags, so we'll loop through
|
||||
// to find them all.
|
||||
while ((style = text.match(re_style))) {
|
||||
let tagsToOpen = [], replaceString = '';
|
||||
if (style[1] && style[1].split) { // Stop throwing errors on empty tags.
|
||||
style = style[1].split("\\"); // Get an array of override commands.
|
||||
for (let j = 1; j < style.length; j++) {
|
||||
// Extract the current tag name.
|
||||
let tagCommand = style[j].match(/[a-zA-Z]+/)[0];
|
||||
// Give special reckognition to one-letter tags.
|
||||
let oneLetter = (tagCommand.length == 1) ? tagCommand : "";
|
||||
// "New" position commands. It is assumed that bottom center position is the default.
|
||||
if (tagCommand === "an") {
|
||||
let posNum = Number(style[j].substring(2, 3));
|
||||
if (Math.floor((posNum - 1) / 3) == 1) {
|
||||
cue.line = 0.5;
|
||||
} else if (Math.floor((posNum - 1) / 3) == 2) {
|
||||
cue.line = 0;
|
||||
}
|
||||
if (posNum % 3 == 1) {
|
||||
cue.align = "start";
|
||||
cue.text = " \r\n"
|
||||
} else if (posNum % 3 == 0) {
|
||||
cue.align = "end";
|
||||
}
|
||||
// Legacy position commands.
|
||||
} else if (oneLetter === "a" && !Number.isNaN(Number(style[j].substring(1, 2)))) {
|
||||
let posNum = Number(style[j].substring(1, 2));
|
||||
if (posNum > 8) {
|
||||
cue.line = 0.5;
|
||||
} else if (posNum > 4) {
|
||||
cue.line = 0;
|
||||
}
|
||||
if ((posNum - 1) % 4 == 0) {
|
||||
cue.align = "start";
|
||||
cue.text = " \r\n"
|
||||
} else if ((posNum - 1) % 4 == 2) {
|
||||
cue.align = "end";
|
||||
}
|
||||
// Map simple text decoration commands to equivalent WebVTT text tags.
|
||||
// NOTE: Strikethrough (the 's' tag) is not supported in WebVTT.
|
||||
} else if (['b', 'i', 'u', 's'].includes(oneLetter)) {
|
||||
if (Number(style[j].substring(1, 2)) === 0
|
||||
// The more elaborate 'b-tag', which we will treat as an on-off selector.
|
||||
|| (style[j].match(/b\d{3}/)
|
||||
&& Number(style[j].match(/b(\d{3})/)[1]) < 500)
|
||||
) {
|
||||
// Closing a tag.
|
||||
if (tagsToClose.includes(oneLetter)) {
|
||||
// Nothing needs to be done if this tag isn't already open.
|
||||
// HTML tags must be nested, so we must ensure that any tag nested inside
|
||||
// the tag being closed are also closed, and then opened again once the
|
||||
// current tag is closed.
|
||||
while (tagsToClose.length > 0) {
|
||||
let nowClosing = tagsToClose.pop();
|
||||
replaceString += '</' + nowClosing + '>';
|
||||
if (nowClosing !== oneLetter) {
|
||||
tagsToOpen.push(nowClosing);
|
||||
} else {
|
||||
// There's no need to close the tags that the current tag
|
||||
// is nested within.
|
||||
break;
|
||||
if (tracks[trackNumber].label == "ass") {
|
||||
// Support for special characters in WebVTT.
|
||||
// For obvious reasons, the ampersand one *must* be first.
|
||||
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
let style, tagsToClose = []; // Places to stash style info.
|
||||
// Subtitles may contain any number of override tags, so we'll loop through
|
||||
// to find them all.
|
||||
while ((style = text.match(re_style))) {
|
||||
let tagsToOpen = [], replaceString = '';
|
||||
if (style[1] && style[1].split) { // Stop throwing errors on empty tags.
|
||||
style = style[1].split("\\"); // Get an array of override commands.
|
||||
for (let j = 1; j < style.length; j++) {
|
||||
// Extract the current tag name.
|
||||
let tagCommand = style[j].match(/[a-zA-Z]+/)[0];
|
||||
// Give special reckognition to one-letter tags.
|
||||
let oneLetter = (tagCommand.length == 1) ? tagCommand : "";
|
||||
// "New" position commands. It is assumed that bottom center position is the default.
|
||||
if (tagCommand === "an") {
|
||||
let posNum = Number(style[j].substring(2, 3));
|
||||
if (Math.floor((posNum - 1) / 3) == 1) {
|
||||
cue.line = 0.5;
|
||||
} else if (Math.floor((posNum - 1) / 3) == 2) {
|
||||
cue.line = 0;
|
||||
}
|
||||
if (posNum % 3 == 1) {
|
||||
cue.align = "start";
|
||||
cue.text = " \r\n"
|
||||
} else if (posNum % 3 == 0) {
|
||||
cue.align = "end";
|
||||
}
|
||||
// Legacy position commands.
|
||||
} else if (oneLetter === "a" && !Number.isNaN(Number(style[j].substring(1, 2)))) {
|
||||
let posNum = Number(style[j].substring(1, 2));
|
||||
if (posNum > 8) {
|
||||
cue.line = 0.5;
|
||||
} else if (posNum > 4) {
|
||||
cue.line = 0;
|
||||
}
|
||||
if ((posNum - 1) % 4 == 0) {
|
||||
cue.align = "start";
|
||||
cue.text = " \r\n"
|
||||
} else if ((posNum - 1) % 4 == 2) {
|
||||
cue.align = "end";
|
||||
}
|
||||
// Map simple text decoration commands to equivalent WebVTT text tags.
|
||||
// NOTE: Strikethrough (the 's' tag) is not supported in WebVTT.
|
||||
} else if (['b', 'i', 'u', 's'].includes(oneLetter)) {
|
||||
if (Number(style[j].substring(1, 2)) === 0
|
||||
// The more elaborate 'b-tag', which we will treat as an on-off selector.
|
||||
|| (style[j].match(/b\d{3}/)
|
||||
&& Number(style[j].match(/b(\d{3})/)[1]) < 500)
|
||||
) {
|
||||
// Closing a tag.
|
||||
if (tagsToClose.includes(oneLetter)) {
|
||||
// Nothing needs to be done if this tag isn't already open.
|
||||
// HTML tags must be nested, so we must ensure that any tag nested inside
|
||||
// the tag being closed are also closed, and then opened again once the
|
||||
// current tag is closed.
|
||||
while (tagsToClose.length > 0) {
|
||||
let nowClosing = tagsToClose.pop();
|
||||
replaceString += '</' + nowClosing + '>';
|
||||
if (nowClosing !== oneLetter) {
|
||||
tagsToOpen.push(nowClosing);
|
||||
} else {
|
||||
// There's no need to close the tags that the current tag
|
||||
// is nested within.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Opening a tag.
|
||||
if (!tagsToClose.includes(oneLetter)) {
|
||||
// Nothing needs to be done if the tag is already open.
|
||||
// If no, place the tag on the bottom of the stack of tags being opened.
|
||||
tagsToOpen.splice(0, 0, oneLetter);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Opening a tag.
|
||||
if (!tagsToClose.includes(oneLetter)) {
|
||||
// Nothing needs to be done if the tag is already open.
|
||||
// If no, place the tag on the bottom of the stack of tags being opened.
|
||||
tagsToOpen.splice(0, 0, oneLetter);
|
||||
} else if (oneLetter === 'r') {
|
||||
// Resetting override tags, by closing all open tags.
|
||||
// TODO: The 'r' tag can also be used to switch to a different named style,
|
||||
// however, named styles haven't been implemented.
|
||||
while (tagsToClose.length > 0) {
|
||||
replaceString += '</' + tagsToClose.pop() + '>';
|
||||
}
|
||||
}
|
||||
} else if (oneLetter === 'r') {
|
||||
// Resetting override tags, by closing all open tags.
|
||||
// TODO: The 'r' tag can also be used to switch to a different named style,
|
||||
// however, named styles haven't been implemented.
|
||||
while (tagsToClose.length > 0) {
|
||||
replaceString += '</' + tagsToClose.pop() + '>';
|
||||
// Insert open-tags for tags in the to-open list.
|
||||
while (tagsToOpen.length > 0) {
|
||||
let nowOpening = tagsToOpen.pop();
|
||||
replaceString += '<' + nowOpening + '>';
|
||||
tagsToClose.push(nowOpening);
|
||||
}
|
||||
}
|
||||
// Insert open-tags for tags in the to-open list.
|
||||
while (tagsToOpen.length > 0) {
|
||||
let nowOpening = tagsToOpen.pop();
|
||||
replaceString += '<' + nowOpening + '>';
|
||||
tagsToClose.push(nowOpening);
|
||||
}
|
||||
}
|
||||
text = text.replace(re_style, replaceString); // Replace override tag.
|
||||
}
|
||||
text = text.replace(re_style, replaceString); // Replace override tag.
|
||||
text = text.replace(re_newline, "\r\n").replace(re_softbreak, " ").replace(
|
||||
re_hardspace, " ");
|
||||
let content = "<v " + result.style + ">" + text
|
||||
while (tagsToClose.length > 0) {
|
||||
content += '</' + tagsToClose.pop() + '>';
|
||||
}
|
||||
cue.text += `${content}\r\n `
|
||||
} else {
|
||||
cue.text = `${text}\r\n `
|
||||
}
|
||||
text = text.replace(re_newline, "\r\n").replace(re_softbreak, " ").replace(
|
||||
re_hardspace, " ");
|
||||
let content = "<v " + result.style + ">" + text
|
||||
while (tagsToClose.length > 0) {
|
||||
content += '</' + tagsToClose.pop() + '>';
|
||||
}
|
||||
cue.text += `${content}\r\n `
|
||||
if (!Object.values(tracks[trackNumber].cues).some(c => c.text == cue.text)) {
|
||||
tracks[trackNumber].addCue(cue)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ const client = new WebTorrent(),
|
|||
],
|
||||
scope = '/app/',
|
||||
sw = navigator.serviceWorker.register('sw.js', { scope })
|
||||
let parser
|
||||
//for debugging
|
||||
function t(a) {
|
||||
switch (a) {
|
||||
|
|
@ -47,7 +46,7 @@ WEBTORRENT_ANNOUNCE = announceList
|
|||
return url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0
|
||||
})
|
||||
var nowPlaying,
|
||||
maxTorrents = 3,
|
||||
maxTorrents = 1,
|
||||
subStream
|
||||
function addTorrent(magnet) {
|
||||
if (client.torrents.length >= maxTorrents) {
|
||||
|
|
@ -56,6 +55,7 @@ function addTorrent(magnet) {
|
|||
halfmoon.toggleModal("tsearch")
|
||||
document.location.href = "#player"
|
||||
client.add(magnet, async function (torrent) {
|
||||
video.src = ""
|
||||
await sw
|
||||
function onProgress() {
|
||||
peers.textContent = torrent.numPeers
|
||||
|
|
@ -90,6 +90,7 @@ function addTorrent(magnet) {
|
|||
videoFile = file
|
||||
}
|
||||
})
|
||||
parser = undefined
|
||||
video.src = `${scope}webtorrent/${torrent.infoHash}/${encodeURI(videoFile.path)}`
|
||||
if (subStream) {
|
||||
subStream.destroy()
|
||||
|
|
@ -128,36 +129,14 @@ function serveFile(file, req) {
|
|||
}
|
||||
|
||||
|
||||
res.headers['Cache-Control'] = 'no-store'
|
||||
res.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate, max-age=0'
|
||||
res.headers['Expires'] = '0'
|
||||
res.body = req.method === 'HEAD' ? '' : 'stream'
|
||||
console.log('set parser', range)
|
||||
|
||||
parser = new MatroskaSubtitles({ prevInstance: parser, offset: range.start })
|
||||
|
||||
parser.once('tracks', function (pTracks) {
|
||||
tracks = []
|
||||
pTracks.forEach(track => {
|
||||
if (track.type == "ass") {
|
||||
tracks[track.number] = video.addTextTrack('captions', track.number, !!track.language ? track.language : track.number)
|
||||
}
|
||||
})
|
||||
if (video.textTracks[0]) {
|
||||
video.textTracks[0].mode = "showing"
|
||||
}
|
||||
})
|
||||
|
||||
parser.once('cues', function () {
|
||||
console.log('seeking ready')
|
||||
})
|
||||
|
||||
parser.on('subtitle', function (subtitle, trackNumber) {
|
||||
subConvt(subtitle, trackNumber)
|
||||
})
|
||||
|
||||
// parser is really a passthrough mkv stream now
|
||||
file.createReadStream(range).pipe(parser)
|
||||
let stream = file.createReadStream(range)
|
||||
parseSubs(range, stream)
|
||||
|
||||
return [res, req.method === 'GET' && parser]
|
||||
return [res, req.method === 'GET' && parser || stream]
|
||||
}
|
||||
|
||||
// kind of a fetch event from service worker but for the main thread.
|
||||
|
|
|
|||
Loading…
Reference in a new issue