2dbcba8846
Fixes #1627
1053 lines
35 KiB
Plaintext
1053 lines
35 KiB
Plaintext
import "pkg:/source/utils/misc.bs"
|
|
import "pkg:/source/api/Items.bs"
|
|
import "pkg:/source/api/UserLibrary.bs"
|
|
import "pkg:/source/api/baserequest.bs"
|
|
import "pkg:/source/utils/config.bs"
|
|
import "pkg:/source/api/Image.bs"
|
|
import "pkg:/source/api/userauth.bs"
|
|
import "pkg:/source/utils/deviceCapabilities.bs"
|
|
|
|
enum SubtitleSelection
|
|
notset = -2
|
|
none = -1
|
|
end enum
|
|
|
|
sub init()
|
|
m.user = AboutMe()
|
|
m.top.functionName = "loadItems"
|
|
end sub
|
|
|
|
sub loadItems()
|
|
' Reset intro tracker in case task gets reused
|
|
m.top.isIntro = false
|
|
|
|
' Only show preroll once per queue
|
|
if m.global.queueManager.callFunc("isPrerollActive")
|
|
' Prerolls not allowed if we're resuming video
|
|
if m.global.queueManager.callFunc("getCurrentItem").startingPoint = 0
|
|
preRoll = GetIntroVideos(m.top.itemId)
|
|
if isValid(preRoll) and preRoll.TotalRecordCount > 0 and isValid(preRoll.items[0])
|
|
' If an error is thrown in the Intros plugin, instead of passing the error they pass the entire rick roll music video.
|
|
' Bypass the music video and treat it as an error message
|
|
if lcase(preRoll.items[0].name) <> "rick roll'd"
|
|
m.global.queueManager.callFunc("push", m.global.queueManager.callFunc("getCurrentItem"))
|
|
m.top.itemId = preRoll.items[0].id
|
|
m.global.queueManager.callFunc("setPrerollStatus", false)
|
|
m.top.isIntro = true
|
|
end if
|
|
end if
|
|
end if
|
|
end if
|
|
|
|
if m.top.selectedAudioStreamIndex = 0
|
|
currentItem = m.global.queueManager.callFunc("getCurrentItem")
|
|
if isValid(currentItem) and isValid(currentItem.json)
|
|
m.top.selectedAudioStreamIndex = FindPreferredAudioStream(currentItem.json.MediaStreams)
|
|
end if
|
|
end if
|
|
|
|
id = m.top.itemId
|
|
mediaSourceId = invalid
|
|
audio_stream_idx = m.top.selectedAudioStreamIndex
|
|
forceTranscoding = false
|
|
|
|
m.top.content = [LoadItems_VideoPlayer(id, mediaSourceId, audio_stream_idx, forceTranscoding)]
|
|
end sub
|
|
|
|
function LoadItems_VideoPlayer(id as string, mediaSourceId = invalid as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean) as dynamic
|
|
|
|
video = {}
|
|
video.id = id
|
|
video.content = createObject("RoSGNode", "ContentNode")
|
|
|
|
LoadItems_AddVideoContent(video, mediaSourceId, audio_stream_idx, forceTranscoding)
|
|
|
|
if video.content = invalid
|
|
return invalid
|
|
end if
|
|
|
|
return video
|
|
end function
|
|
|
|
sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_stream_idx = 1 as integer, forceTranscoding = false as boolean)
|
|
|
|
meta = ItemMetaData(video.id)
|
|
subtitle_idx = m.top.selectedSubtitleIndex
|
|
|
|
if not isValid(meta)
|
|
video.errorMsg = "Error loading metadata"
|
|
video.content = invalid
|
|
return
|
|
end if
|
|
|
|
videotype = LCase(meta.type)
|
|
|
|
' Check for any Live TV streams coming from other places other than the TV Guide
|
|
if isValid(meta.json) and isValid(meta.json.ChannelId)
|
|
if isValid(meta.json.EpisodeTitle)
|
|
meta.title = meta.json.EpisodeTitle
|
|
else if isValid(meta.json.Name)
|
|
meta.title = meta.json.Name
|
|
end if
|
|
meta.live = true
|
|
if LCase(meta.json.type) = "program"
|
|
video.id = meta.json.ChannelId
|
|
else
|
|
video.id = meta.json.id
|
|
end if
|
|
end if
|
|
|
|
if videotype = "episode" or videotype = "series"
|
|
video.content.contenttype = "episode"
|
|
end if
|
|
|
|
video.chapters = meta.json.Chapters
|
|
video.content.title = meta.title
|
|
video.showID = meta.showID
|
|
|
|
user = AboutMe()
|
|
if user.Configuration.EnableNextEpisodeAutoPlay
|
|
if LCase(m.top.itemType) = "episode"
|
|
addNextEpisodesToQueue(video.showID)
|
|
end if
|
|
end if
|
|
|
|
playbackPosition = 0!
|
|
|
|
currentItem = m.global.queueManager.callFunc("getCurrentItem")
|
|
|
|
if isValid(currentItem) and isValid(currentItem.startingPoint)
|
|
playbackPosition = currentItem.startingPoint
|
|
end if
|
|
|
|
' PlayStart requires the time to be in seconds
|
|
video.content.PlayStart = int(playbackPosition / 10000000)
|
|
|
|
if not isValid(mediaSourceId) then mediaSourceId = video.id
|
|
if meta.live then mediaSourceId = ""
|
|
|
|
m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition)
|
|
if not isValid(m.playbackInfo)
|
|
video.errorMsg = "Error loading playback info"
|
|
video.content = invalid
|
|
return
|
|
end if
|
|
|
|
addSubtitlesToVideo(video, meta)
|
|
|
|
' Enable default subtitle track
|
|
if subtitle_idx = SubtitleSelection.notset
|
|
defaultSubtitleIndex = defaultSubtitleTrackFromVid(video.id)
|
|
|
|
if defaultSubtitleIndex <> SubtitleSelection.none
|
|
video.SelectedSubtitle = defaultSubtitleIndex
|
|
subtitle_idx = defaultSubtitleIndex
|
|
|
|
m.playbackInfo = ItemPostPlaybackInfo(video.id, mediaSourceId, audio_stream_idx, subtitle_idx, playbackPosition)
|
|
if not isValid(m.playbackInfo)
|
|
video.errorMsg = "Error loading playback info"
|
|
video.content = invalid
|
|
return
|
|
end if
|
|
|
|
addSubtitlesToVideo(video, meta)
|
|
else
|
|
video.SelectedSubtitle = subtitle_idx
|
|
end if
|
|
else
|
|
video.SelectedSubtitle = subtitle_idx
|
|
end if
|
|
|
|
video.videoId = video.id
|
|
video.mediaSourceId = mediaSourceId
|
|
video.audioIndex = audio_stream_idx
|
|
|
|
video.PlaySessionId = m.playbackInfo.PlaySessionId
|
|
|
|
if meta.live
|
|
video.content.live = true
|
|
video.content.StreamFormat = "hls"
|
|
end if
|
|
|
|
video.container = getContainerType(meta)
|
|
|
|
if not isValid(m.playbackInfo.MediaSources[0])
|
|
m.playbackInfo = meta.json
|
|
end if
|
|
|
|
if meta.live
|
|
video.transcodeParams = {
|
|
"MediaSourceId": m.playbackInfo.MediaSources[0].Id,
|
|
"LiveStreamId": m.playbackInfo.MediaSources[0].LiveStreamId,
|
|
"PlaySessionId": video.PlaySessionId
|
|
}
|
|
end if
|
|
|
|
|
|
' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles
|
|
video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay
|
|
fully_external = false
|
|
|
|
|
|
' For h264/hevc video, Roku spec states that it supports specfic encoding levels
|
|
' The device can decode content with a Higher Encoding level but may play it back with certain
|
|
' artifacts. If the user preference is set, and the only reason the server says we need to
|
|
' transcode is that the Encoding Level is not supported, then try to direct play but silently
|
|
' fall back to the transcode if that fails.
|
|
if m.playbackInfo.MediaSources[0].MediaStreams.Count() > 0 and meta.live = false
|
|
tryDirectPlay = m.global.session.user.settings["playback.tryDirect.h264ProfileLevel"] and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "h264"
|
|
tryDirectPlay = tryDirectPlay or (m.global.session.user.settings["playback.tryDirect.hevcProfileLevel"] and m.playbackInfo.MediaSources[0].MediaStreams[0].codec = "hevc")
|
|
if tryDirectPlay and isValid(m.playbackInfo.MediaSources[0].TranscodingUrl) and forceTranscoding = false
|
|
transcodingReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl)
|
|
if transcodingReasons.Count() = 1 and transcodingReasons[0] = "VideoLevelNotSupported"
|
|
video.directPlaySupported = true
|
|
video.transcodeAvailable = true
|
|
end if
|
|
end if
|
|
end if
|
|
|
|
if video.directPlaySupported
|
|
video.isTranscoded = false
|
|
addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
|
else
|
|
if m.playbackInfo.MediaSources[0].TranscodingUrl = invalid
|
|
' If server does not provide a transcode URL, display a message to the user
|
|
m.global.sceneManager.callFunc("userMessage", tr("Error Getting Playback Information"), tr("An error was encountered while playing this item. Server did not provide required transcoding data."))
|
|
video.errorMsg = "Error getting playback information"
|
|
video.content = invalid
|
|
return
|
|
end if
|
|
' Get transcoding reason
|
|
video.transcodeReasons = getTranscodeReasons(m.playbackInfo.MediaSources[0].TranscodingUrl)
|
|
video.content.url = buildURL(m.playbackInfo.MediaSources[0].TranscodingUrl)
|
|
video.isTranscoded = true
|
|
end if
|
|
|
|
setCertificateAuthority(video.content)
|
|
video.audioTrack = (audio_stream_idx + 1).ToStr() ' Roku's track indexes count from 1. Our index is zero based
|
|
|
|
if not fully_external
|
|
video.content = authRequest(video.content)
|
|
end if
|
|
end sub
|
|
|
|
' defaultSubtitleTrackFromVid: Identifies the default subtitle track given video id
|
|
'
|
|
' @param {dynamic} videoID - id of video user is playing
|
|
' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found
|
|
function defaultSubtitleTrackFromVid(videoID) as integer
|
|
if m.global.session.user.configuration.SubtitleMode = "None"
|
|
return SubtitleSelection.none ' No subtitles desired: return none
|
|
end if
|
|
|
|
meta = ItemMetaData(videoID)
|
|
|
|
if not isValid(meta) then return SubtitleSelection.none
|
|
if not isValid(meta.json) then return SubtitleSelection.none
|
|
if not isValidAndNotEmpty(meta.json.mediaSources) then return SubtitleSelection.none
|
|
if not isValidAndNotEmpty(meta.json.MediaSources[0].MediaStreams) then return SubtitleSelection.none
|
|
|
|
subtitles = sortSubtitles(meta.id, meta.json.MediaSources[0].MediaStreams)
|
|
selectedAudioLanguage = meta.json.MediaSources[0].MediaStreams[m.top.selectedAudioStreamIndex].Language ?? ""
|
|
|
|
defaultTextSubs = defaultSubtitleTrack(subtitles["text"], selectedAudioLanguage, true) ' Find correct subtitle track (forced text)
|
|
if defaultTextSubs <> SubtitleSelection.none
|
|
return defaultTextSubs
|
|
end if
|
|
|
|
if not m.global.session.user.settings["playback.subs.onlytext"]
|
|
return defaultSubtitleTrack(subtitles["all"], selectedAudioLanguage) ' if no appropriate text subs exist, allow non-text
|
|
end if
|
|
|
|
return SubtitleSelection.none
|
|
end function
|
|
|
|
' defaultSubtitleTrack:
|
|
'
|
|
' @param {dynamic} sortedSubtitles - array of subtitles sorted by type and language
|
|
' @param {string} selectedAudioLanguage - language for selected audio track
|
|
' @param {boolean} [requireText=false] - indicates if only text subtitles should be considered
|
|
' @return {integer} indicating the default track's server-side index. Defaults to {SubtitleSelection.none} is one is not found
|
|
function defaultSubtitleTrack(sortedSubtitles, selectedAudioLanguage as string, requireText = false as boolean) as integer
|
|
userConfig = m.global.session.user.configuration
|
|
|
|
subtitleMode = isValid(userConfig.SubtitleMode) ? LCase(userConfig.SubtitleMode) : ""
|
|
|
|
allowSmartMode = false
|
|
|
|
' Only evaluate selected audio language if we have a value
|
|
if selectedAudioLanguage <> ""
|
|
allowSmartMode = selectedAudioLanguage <> userConfig.SubtitleLanguagePreference
|
|
end if
|
|
|
|
for each item in sortedSubtitles
|
|
' Only auto-select subtitle if language matches SubtitleLanguagePreference
|
|
languageMatch = true
|
|
if userConfig.SubtitleLanguagePreference <> ""
|
|
languageMatch = (userConfig.SubtitleLanguagePreference = item.Track.Language)
|
|
end if
|
|
|
|
' Ensure textuality of subtitle matches preferenced passed as arg
|
|
matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText)
|
|
|
|
if languageMatch and matchTextReq
|
|
if subtitleMode = "default" and (item.isForced or item.IsDefault)
|
|
' Return first forced or default subtitle track
|
|
return item.Index
|
|
else if subtitleMode = "always"
|
|
' Return the first found subtitle track
|
|
return item.Index
|
|
else if subtitleMode = "onlyforced" and item.IsForced
|
|
' Return first forced subtitle track
|
|
return item.Index
|
|
else if subtitleMode = "smart" and allowSmartMode
|
|
' Return the first found subtitle track
|
|
return item.Index
|
|
end if
|
|
end if
|
|
end for
|
|
|
|
' User has chosed smart subtitle mode
|
|
' We already attempted to load subtitles in preferred language, but none were found.
|
|
' Fall back to default behaviour while ignoring preferredlanguage
|
|
if subtitleMode = "smart" and allowSmartMode
|
|
for each item in sortedSubtitles
|
|
' Ensure textuality of subtitle matches preferenced passed as arg
|
|
matchTextReq = ((requireText and item.IsTextSubtitleStream) or not requireText)
|
|
if matchTextReq
|
|
if item.isForced or item.IsDefault
|
|
' Return first forced or default subtitle track
|
|
return item.Index
|
|
end if
|
|
end if
|
|
end for
|
|
end if
|
|
|
|
return SubtitleSelection.none ' Keep current default behavior of "None", if no correct subtitle is identified
|
|
end function
|
|
|
|
sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external)
|
|
protocol = LCase(m.playbackInfo.MediaSources[0].Protocol)
|
|
if protocol <> "file"
|
|
uri = parseUrl(m.playbackInfo.MediaSources[0].Path)
|
|
if isLocalhost(uri[2])
|
|
' if the domain of the URI is local to the server,
|
|
' create a new URI by appending the received path to the server URL
|
|
' later we will substitute the users provided URL for this case
|
|
video.content.url = buildURL(uri[4])
|
|
else
|
|
fully_external = true
|
|
video.content.url = m.playbackInfo.MediaSources[0].Path
|
|
end if
|
|
else
|
|
params = {
|
|
"Static": "true",
|
|
"Container": video.container,
|
|
"PlaySessionId": video.PlaySessionId,
|
|
"AudioStreamIndex": audio_stream_idx
|
|
}
|
|
|
|
if mediaSourceId <> ""
|
|
params.MediaSourceId = mediaSourceId
|
|
end if
|
|
|
|
video.content.url = buildURL(Substitute("Videos/{0}/stream", video.id), params)
|
|
end if
|
|
end sub
|
|
|
|
sub addSubtitlesToVideo(video, meta)
|
|
subtitles = sortSubtitles(meta.id, m.playbackInfo.MediaSources[0].MediaStreams)
|
|
safesubs = subtitles["all"]
|
|
subtitleTracks = []
|
|
|
|
if m.global.session.user.settings["playback.subs.onlytext"] = true
|
|
safesubs = subtitles["text"]
|
|
end if
|
|
|
|
for each subtitle in safesubs
|
|
subtitleTracks.push(subtitle.track)
|
|
end for
|
|
|
|
video.content.SubtitleTracks = subtitleTracks
|
|
video.fullSubtitleData = safesubs
|
|
end sub
|
|
|
|
|
|
'
|
|
' Extract array of Transcode Reasons from the content URL
|
|
' @returns Array of Strings
|
|
function getTranscodeReasons(url as string) as object
|
|
|
|
regex = CreateObject("roRegex", "&TranscodeReasons=([^&]*)", "")
|
|
match = regex.Match(url)
|
|
|
|
if match.count() > 1
|
|
return match[1].Split(",")
|
|
end if
|
|
|
|
return []
|
|
end function
|
|
|
|
function directPlaySupported(meta as object) as boolean
|
|
devinfo = CreateObject("roDeviceInfo")
|
|
if isValid(meta.json.MediaSources[0]) and meta.json.MediaSources[0].SupportsDirectPlay = false
|
|
return false
|
|
end if
|
|
|
|
if meta.json.MediaStreams[0] = invalid
|
|
return false
|
|
end if
|
|
|
|
streamInfo = { Codec: meta.json.MediaStreams[0].codec }
|
|
if isValid(meta.json.MediaStreams[0].Profile) and meta.json.MediaStreams[0].Profile.len() > 0
|
|
streamInfo.Profile = LCase(meta.json.MediaStreams[0].Profile)
|
|
end if
|
|
if isValid(meta.json.MediaSources[0].container) and meta.json.MediaSources[0].container.len() > 0
|
|
'CanDecodeVideo() requires the .container to be format: “mp4”, “hls”, “mkv”, “ism”, “dash”, “ts” if its to direct stream
|
|
if meta.json.MediaSources[0].container = "mov"
|
|
streamInfo.Container = "mp4"
|
|
else
|
|
streamInfo.Container = meta.json.MediaSources[0].container
|
|
end if
|
|
end if
|
|
|
|
decodeResult = devinfo.CanDecodeVideo(streamInfo)
|
|
return decodeResult <> invalid and decodeResult.result
|
|
|
|
end function
|
|
|
|
function getContainerType(meta as object) as string
|
|
' Determine the file type of the video file source
|
|
if meta.json.mediaSources = invalid then return ""
|
|
|
|
container = meta.json.mediaSources[0].container
|
|
if container = invalid
|
|
container = ""
|
|
else if container = "m4v" or container = "mov"
|
|
container = "mp4"
|
|
end if
|
|
|
|
return container
|
|
end function
|
|
|
|
' Add next episodes to the playback queue
|
|
sub addNextEpisodesToQueue(showID)
|
|
' Don't queue next episodes if we already have a playback queue
|
|
maxQueueCount = 1
|
|
|
|
if m.top.isIntro
|
|
maxQueueCount = 2
|
|
end if
|
|
|
|
if m.global.queueManager.callFunc("getCount") > maxQueueCount then return
|
|
|
|
videoID = m.top.itemId
|
|
|
|
' If first item is an intro video, use the next item in the queue
|
|
if m.top.isIntro
|
|
currentVideo = m.global.queueManager.callFunc("getItemByIndex", 1)
|
|
|
|
if isValid(currentVideo) and isValid(currentVideo.id)
|
|
videoID = currentVideo.id
|
|
|
|
' Override showID value since it's for the intro video
|
|
meta = ItemMetaData(videoID)
|
|
if isValid(meta)
|
|
showID = meta.showID
|
|
end if
|
|
end if
|
|
end if
|
|
|
|
url = Substitute("Shows/{0}/Episodes", showID)
|
|
urlParams = { "UserId": m.global.session.user.id }
|
|
urlParams.Append({ "StartItemId": videoID })
|
|
urlParams.Append({ "Limit": 50 })
|
|
resp = APIRequest(url, urlParams)
|
|
data = getJson(resp)
|
|
|
|
if isValid(data) and data.Items.Count() > 1
|
|
for i = 1 to data.Items.Count() - 1
|
|
m.global.queueManager.callFunc("push", data.Items[i])
|
|
end for
|
|
end if
|
|
end sub
|
|
|
|
'Checks available subtitle tracks and puts subtitles in forced, default, and non-default/forced but preferred language at the top
|
|
function sortSubtitles(id as string, MediaStreams)
|
|
tracks = { "forced": [], "default": [], "normal": [], "text": [] }
|
|
'Too many args for using substitute
|
|
prefered_lang = m.global.session.user.configuration.SubtitleLanguagePreference
|
|
for each stream in MediaStreams
|
|
if stream.type = "Subtitle"
|
|
|
|
url = ""
|
|
if isValid(stream.DeliveryUrl)
|
|
url = buildURL(stream.DeliveryUrl)
|
|
end if
|
|
|
|
stream = {
|
|
"Track": { "Language": stream.language, "Description": stream.displaytitle, "TrackName": url },
|
|
"IsTextSubtitleStream": stream.IsTextSubtitleStream,
|
|
"Index": stream.index,
|
|
"IsDefault": stream.IsDefault,
|
|
"IsForced": stream.IsForced,
|
|
"IsExternal": stream.IsExternal,
|
|
"IsEncoded": stream.DeliveryMethod = "Encode"
|
|
}
|
|
|
|
if stream.isForced
|
|
trackType = "forced"
|
|
else if stream.IsDefault
|
|
trackType = "default"
|
|
else
|
|
trackType = "normal"
|
|
end if
|
|
|
|
if prefered_lang <> "" and prefered_lang = stream.Track.Language
|
|
tracks[trackType].unshift(stream)
|
|
|
|
if stream.IsTextSubtitleStream
|
|
tracks["text"].unshift(stream)
|
|
end if
|
|
else
|
|
tracks[trackType].push(stream)
|
|
|
|
if stream.IsTextSubtitleStream
|
|
tracks["text"].push(stream)
|
|
end if
|
|
end if
|
|
end if
|
|
end for
|
|
|
|
tracks["default"].append(tracks["normal"])
|
|
tracks["forced"].append(tracks["default"])
|
|
|
|
return { "all": tracks["forced"], "text": tracks["text"] }
|
|
end function
|
|
|
|
function FindPreferredAudioStream(streams as dynamic) as integer
|
|
preferredLanguage = m.user.Configuration.AudioLanguagePreference
|
|
playDefault = m.user.Configuration.PlayDefaultAudioTrack
|
|
|
|
if playDefault <> invalid and playDefault = true
|
|
return 1
|
|
end if
|
|
|
|
' Do we already have the MediaStreams or not?
|
|
if streams = invalid
|
|
url = Substitute("Users/{0}/Items/{1}", m.user.id, m.top.itemId)
|
|
resp = APIRequest(url)
|
|
jsonResponse = getJson(resp)
|
|
|
|
if jsonResponse = invalid or jsonResponse.MediaStreams = invalid then return 1
|
|
|
|
streams = jsonResponse.MediaStreams
|
|
end if
|
|
|
|
if preferredLanguage <> invalid
|
|
for i = 0 to streams.Count() - 1
|
|
if LCase(streams[i].Type) = "audio"
|
|
if streams[i].Language <> invalid and LCase(streams[i].Language) = LCase(preferredLanguage)
|
|
return i
|
|
end if
|
|
end if
|
|
end for
|
|
end if
|
|
|
|
return 1
|
|
end function
|
|
|
|
function getSubtitleLanguages()
|
|
return {
|
|
"aar": "Afar",
|
|
"abk": "Abkhazian",
|
|
"ace": "Achinese",
|
|
"ach": "Acoli",
|
|
"ada": "Adangme",
|
|
"ady": "Adyghe; Adygei",
|
|
"afa": "Afro-Asiatic languages",
|
|
"afh": "Afrihili",
|
|
"afr": "Afrikaans",
|
|
"ain": "Ainu",
|
|
"aka": "Akan",
|
|
"akk": "Akkadian",
|
|
"alb": "Albanian",
|
|
"ale": "Aleut",
|
|
"alg": "Algonquian languages",
|
|
"alt": "Southern Altai",
|
|
"amh": "Amharic",
|
|
"ang": "English, Old (ca.450-1100)",
|
|
"anp": "Angika",
|
|
"apa": "Apache languages",
|
|
"ara": "Arabic",
|
|
"arc": "Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)",
|
|
"arg": "Aragonese",
|
|
"arm": "Armenian",
|
|
"arn": "Mapudungun; Mapuche",
|
|
"arp": "Arapaho",
|
|
"art": "Artificial languages",
|
|
"arw": "Arawak",
|
|
"asm": "Assamese",
|
|
"ast": "Asturian; Bable; Leonese; Asturleonese",
|
|
"ath": "Athapascan languages",
|
|
"aus": "Australian languages",
|
|
"ava": "Avaric",
|
|
"ave": "Avestan",
|
|
"awa": "Awadhi",
|
|
"aym": "Aymara",
|
|
"aze": "Azerbaijani",
|
|
"bad": "Banda languages",
|
|
"bai": "Bamileke languages",
|
|
"bak": "Bashkir",
|
|
"bal": "Baluchi",
|
|
"bam": "Bambara",
|
|
"ban": "Balinese",
|
|
"baq": "Basque",
|
|
"bas": "Basa",
|
|
"bat": "Baltic languages",
|
|
"bej": "Beja; Bedawiyet",
|
|
"bel": "Belarusian",
|
|
"bem": "Bemba",
|
|
"ben": "Bengali",
|
|
"ber": "Berber languages",
|
|
"bho": "Bhojpuri",
|
|
"bih": "Bihari languages",
|
|
"bik": "Bikol",
|
|
"bin": "Bini; Edo",
|
|
"bis": "Bislama",
|
|
"bla": "Siksika",
|
|
"bnt": "Bantu (Other)",
|
|
"bos": "Bosnian",
|
|
"bra": "Braj",
|
|
"bre": "Breton",
|
|
"btk": "Batak languages",
|
|
"bua": "Buriat",
|
|
"bug": "Buginese",
|
|
"bul": "Bulgarian",
|
|
"bur": "Burmese",
|
|
"byn": "Blin; Bilin",
|
|
"cad": "Caddo",
|
|
"cai": "Central American Indian languages",
|
|
"car": "Galibi Carib",
|
|
"cat": "Catalan; Valencian",
|
|
"cau": "Caucasian languages",
|
|
"ceb": "Cebuano",
|
|
"cel": "Celtic languages",
|
|
"cha": "Chamorro",
|
|
"chb": "Chibcha",
|
|
"che": "Chechen",
|
|
"chg": "Chagatai",
|
|
"chi": "Chinese",
|
|
"chk": "Chuukese",
|
|
"chm": "Mari",
|
|
"chn": "Chinook jargon",
|
|
"cho": "Choctaw",
|
|
"chp": "Chipewyan; Dene Suline",
|
|
"chr": "Cherokee",
|
|
"chu": "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic",
|
|
"chv": "Chuvash",
|
|
"chy": "Cheyenne",
|
|
"cmc": "Chamic languages",
|
|
"cop": "Coptic",
|
|
"cor": "Cornish",
|
|
"cos": "Corsican",
|
|
"cpe": "Creoles and pidgins, English based",
|
|
"cpf": "Creoles and pidgins, French-based ",
|
|
"cpp": "Creoles and pidgins, Portuguese-based ",
|
|
"cre": "Cree",
|
|
"crh": "Crimean Tatar; Crimean Turkish",
|
|
"crp": "Creoles and pidgins ",
|
|
"csb": "Kashubian",
|
|
"cus": "Cushitic languages",
|
|
"cze": "Czech",
|
|
"dak": "Dakota",
|
|
"dan": "Danish",
|
|
"dar": "Dargwa",
|
|
"day": "Land Dayak languages",
|
|
"del": "Delaware",
|
|
"den": "Slave (Athapascan)",
|
|
"dgr": "Dogrib",
|
|
"din": "Dinka",
|
|
"div": "Divehi; Dhivehi; Maldivian",
|
|
"doi": "Dogri",
|
|
"dra": "Dravidian languages",
|
|
"dsb": "Lower Sorbian",
|
|
"dua": "Duala",
|
|
"dum": "Dutch, Middle (ca.1050-1350)",
|
|
"dut": "Dutch; Flemish",
|
|
"dyu": "Dyula",
|
|
"dzo": "Dzongkha",
|
|
"efi": "Efik",
|
|
"egy": "Egyptian (Ancient)",
|
|
"eka": "Ekajuk",
|
|
"elx": "Elamite",
|
|
"eng": "English",
|
|
"enm": "English, Middle (1100-1500)",
|
|
"epo": "Esperanto",
|
|
"est": "Estonian",
|
|
"ewe": "Ewe",
|
|
"ewo": "Ewondo",
|
|
"fan": "Fang",
|
|
"fao": "Faroese",
|
|
"fat": "Fanti",
|
|
"fij": "Fijian",
|
|
"fil": "Filipino; Pilipino",
|
|
"fin": "Finnish",
|
|
"fiu": "Finno-Ugrian languages",
|
|
"fon": "Fon",
|
|
"fre": "French",
|
|
"frm": "French, Middle (ca.1400-1600)",
|
|
"fro": "French, Old (842-ca.1400)",
|
|
"frc": "French (Canada)",
|
|
"frr": "Northern Frisian",
|
|
"frs": "Eastern Frisian",
|
|
"fry": "Western Frisian",
|
|
"ful": "Fulah",
|
|
"fur": "Friulian",
|
|
"gaa": "Ga",
|
|
"gay": "Gayo",
|
|
"gba": "Gbaya",
|
|
"gem": "Germanic languages",
|
|
"geo": "Georgian",
|
|
"ger": "German",
|
|
"gez": "Geez",
|
|
"gil": "Gilbertese",
|
|
"gla": "Gaelic; Scottish Gaelic",
|
|
"gle": "Irish",
|
|
"glg": "Galician",
|
|
"glv": "Manx",
|
|
"gmh": "German, Middle High (ca.1050-1500)",
|
|
"goh": "German, Old High (ca.750-1050)",
|
|
"gon": "Gondi",
|
|
"gor": "Gorontalo",
|
|
"got": "Gothic",
|
|
"grb": "Grebo",
|
|
"grc": "Greek, Ancient (to 1453)",
|
|
"gre": "Greek, Modern (1453-)",
|
|
"grn": "Guarani",
|
|
"gsw": "Swiss German; Alemannic; Alsatian",
|
|
"guj": "Gujarati",
|
|
"gwi": "Gwich'in",
|
|
"hai": "Haida",
|
|
"hat": "Haitian; Haitian Creole",
|
|
"hau": "Hausa",
|
|
"haw": "Hawaiian",
|
|
"heb": "Hebrew",
|
|
"her": "Herero",
|
|
"hil": "Hiligaynon",
|
|
"him": "Himachali languages; Western Pahari languages",
|
|
"hin": "Hindi",
|
|
"hit": "Hittite",
|
|
"hmn": "Hmong; Mong",
|
|
"hmo": "Hiri Motu",
|
|
"hrv": "Croatian",
|
|
"hsb": "Upper Sorbian",
|
|
"hun": "Hungarian",
|
|
"hup": "Hupa",
|
|
"iba": "Iban",
|
|
"ibo": "Igbo",
|
|
"ice": "Icelandic",
|
|
"ido": "Ido",
|
|
"iii": "Sichuan Yi; Nuosu",
|
|
"ijo": "Ijo languages",
|
|
"iku": "Inuktitut",
|
|
"ile": "Interlingue; Occidental",
|
|
"ilo": "Iloko",
|
|
"ina": "Interlingua (International Auxiliary Language Association)",
|
|
"inc": "Indic languages",
|
|
"ind": "Indonesian",
|
|
"ine": "Indo-European languages",
|
|
"inh": "Ingush",
|
|
"ipk": "Inupiaq",
|
|
"ira": "Iranian languages",
|
|
"iro": "Iroquoian languages",
|
|
"ita": "Italian",
|
|
"jav": "Javanese",
|
|
"jbo": "Lojban",
|
|
"jpn": "Japanese",
|
|
"jpr": "Judeo-Persian",
|
|
"jrb": "Judeo-Arabic",
|
|
"kaa": "Kara-Kalpak",
|
|
"kab": "Kabyle",
|
|
"kac": "Kachin; Jingpho",
|
|
"kal": "Kalaallisut; Greenlandic",
|
|
"kam": "Kamba",
|
|
"kan": "Kannada",
|
|
"kar": "Karen languages",
|
|
"kas": "Kashmiri",
|
|
"kau": "Kanuri",
|
|
"kaw": "Kawi",
|
|
"kaz": "Kazakh",
|
|
"kbd": "Kabardian",
|
|
"kha": "Khasi",
|
|
"khi": "Khoisan languages",
|
|
"khm": "Central Khmer",
|
|
"kho": "Khotanese; Sakan",
|
|
"kik": "Kikuyu; Gikuyu",
|
|
"kin": "Kinyarwanda",
|
|
"kir": "Kirghiz; Kyrgyz",
|
|
"kmb": "Kimbundu",
|
|
"kok": "Konkani",
|
|
"kom": "Komi",
|
|
"kon": "Kongo",
|
|
"kor": "Korean",
|
|
"kos": "Kosraean",
|
|
"kpe": "Kpelle",
|
|
"krc": "Karachay-Balkar",
|
|
"krl": "Karelian",
|
|
"kro": "Kru languages",
|
|
"kru": "Kurukh",
|
|
"kua": "Kuanyama; Kwanyama",
|
|
"kum": "Kumyk",
|
|
"kur": "Kurdish",
|
|
"kut": "Kutenai",
|
|
"lad": "Ladino",
|
|
"lah": "Lahnda",
|
|
"lam": "Lamba",
|
|
"lao": "Lao",
|
|
"lat": "Latin",
|
|
"lav": "Latvian",
|
|
"lez": "Lezghian",
|
|
"lim": "Limburgan; Limburger; Limburgish",
|
|
"lin": "Lingala",
|
|
"lit": "Lithuanian",
|
|
"lol": "Mongo",
|
|
"loz": "Lozi",
|
|
"ltz": "Luxembourgish; Letzeburgesch",
|
|
"lua": "Luba-Lulua",
|
|
"lub": "Luba-Katanga",
|
|
"lug": "Ganda",
|
|
"lui": "Luiseno",
|
|
"lun": "Lunda",
|
|
"luo": "Luo (Kenya and Tanzania)",
|
|
"lus": "Lushai",
|
|
"mac": "Macedonian",
|
|
"mad": "Madurese",
|
|
"mag": "Magahi",
|
|
"mah": "Marshallese",
|
|
"mai": "Maithili",
|
|
"mak": "Makasar",
|
|
"mal": "Malayalam",
|
|
"man": "Mandingo",
|
|
"mao": "Maori",
|
|
"map": "Austronesian languages",
|
|
"mar": "Marathi",
|
|
"mas": "Masai",
|
|
"may": "Malay",
|
|
"mdf": "Moksha",
|
|
"mdr": "Mandar",
|
|
"men": "Mende",
|
|
"mga": "Irish, Middle (900-1200)",
|
|
"mic": "Mi'kmaq; Micmac",
|
|
"min": "Minangkabau",
|
|
"mis": "Uncoded languages",
|
|
"mkh": "Mon-Khmer languages",
|
|
"mlg": "Malagasy",
|
|
"mlt": "Maltese",
|
|
"mnc": "Manchu",
|
|
"mni": "Manipuri",
|
|
"mno": "Manobo languages",
|
|
"moh": "Mohawk",
|
|
"mon": "Mongolian",
|
|
"mos": "Mossi",
|
|
"mul": "Multiple languages",
|
|
"mun": "Munda languages",
|
|
"mus": "Creek",
|
|
"mwl": "Mirandese",
|
|
"mwr": "Marwari",
|
|
"myn": "Mayan languages",
|
|
"myv": "Erzya",
|
|
"nah": "Nahuatl languages",
|
|
"nai": "North American Indian languages",
|
|
"nap": "Neapolitan",
|
|
"nau": "Nauru",
|
|
"nav": "Navajo; Navaho",
|
|
"nbl": "Ndebele, South; South Ndebele",
|
|
"nde": "Ndebele, North; North Ndebele",
|
|
"ndo": "Ndonga",
|
|
"nds": "Low German; Low Saxon; German, Low; Saxon, Low",
|
|
"nep": "Nepali",
|
|
"new": "Nepal Bhasa; Newari",
|
|
"nia": "Nias",
|
|
"nic": "Niger-Kordofanian languages",
|
|
"niu": "Niuean",
|
|
"nno": "Norwegian Nynorsk; Nynorsk, Norwegian",
|
|
"nob": "Bokmål, Norwegian; Norwegian Bokmål",
|
|
"nog": "Nogai",
|
|
"non": "Norse, Old",
|
|
"nor": "Norwegian",
|
|
"nqo": "N'Ko",
|
|
"nso": "Pedi; Sepedi; Northern Sotho",
|
|
"nub": "Nubian languages",
|
|
"nwc": "Classical Newari; Old Newari; Classical Nepal Bhasa",
|
|
"nya": "Chichewa; Chewa; Nyanja",
|
|
"nym": "Nyamwezi",
|
|
"nyn": "Nyankole",
|
|
"nyo": "Nyoro",
|
|
"nzi": "Nzima",
|
|
"oci": "Occitan (post 1500); Provençal",
|
|
"oji": "Ojibwa",
|
|
"ori": "Oriya",
|
|
"orm": "Oromo",
|
|
"osa": "Osage",
|
|
"oss": "Ossetian; Ossetic",
|
|
"ota": "Turkish, Ottoman (1500-1928)",
|
|
"oto": "Otomian languages",
|
|
"paa": "Papuan languages",
|
|
"pag": "Pangasinan",
|
|
"pal": "Pahlavi",
|
|
"pam": "Pampanga; Kapampangan",
|
|
"pan": "Panjabi; Punjabi",
|
|
"pap": "Papiamento",
|
|
"pau": "Palauan",
|
|
"peo": "Persian, Old (ca.600-400 B.C.)",
|
|
"per": "Persian",
|
|
"phi": "Philippine languages",
|
|
"phn": "Phoenician",
|
|
"pli": "Pali",
|
|
"pol": "Polish",
|
|
"pon": "Pohnpeian",
|
|
"por": "Portuguese",
|
|
"pob": "Portuguese (Brazil)",
|
|
"pra": "Prakrit languages",
|
|
"pro": "Provençal, Old (to 1500)",
|
|
"pus": "Pushto; Pashto",
|
|
"qaa-qtz": "Reserved for local use",
|
|
"que": "Quechua",
|
|
"raj": "Rajasthani",
|
|
"rap": "Rapanui",
|
|
"rar": "Rarotongan; Cook Islands Maori",
|
|
"roa": "Romance languages",
|
|
"roh": "Romansh",
|
|
"rom": "Romany",
|
|
"rum": "Romanian; Moldavian; Moldovan",
|
|
"run": "Rundi",
|
|
"rup": "Aromanian; Arumanian; Macedo-Romanian",
|
|
"rus": "Russian",
|
|
"sad": "Sandawe",
|
|
"sag": "Sango",
|
|
"sah": "Yakut",
|
|
"sai": "South American Indian (Other)",
|
|
"sal": "Salishan languages",
|
|
"sam": "Samaritan Aramaic",
|
|
"san": "Sanskrit",
|
|
"sas": "Sasak",
|
|
"sat": "Santali",
|
|
"scn": "Sicilian",
|
|
"sco": "Scots",
|
|
"sel": "Selkup",
|
|
"sem": "Semitic languages",
|
|
"sga": "Irish, Old (to 900)",
|
|
"sgn": "Sign Languages",
|
|
"shn": "Shan",
|
|
"sid": "Sidamo",
|
|
"sin": "Sinhala; Sinhalese",
|
|
"sio": "Siouan languages",
|
|
"sit": "Sino-Tibetan languages",
|
|
"sla": "Slavic languages",
|
|
"slo": "Slovak",
|
|
"slv": "Slovenian",
|
|
"sma": "Southern Sami",
|
|
"sme": "Northern Sami",
|
|
"smi": "Sami languages",
|
|
"smj": "Lule Sami",
|
|
"smn": "Inari Sami",
|
|
"smo": "Samoan",
|
|
"sms": "Skolt Sami",
|
|
"sna": "Shona",
|
|
"snd": "Sindhi",
|
|
"snk": "Soninke",
|
|
"sog": "Sogdian",
|
|
"som": "Somali",
|
|
"son": "Songhai languages",
|
|
"sot": "Sotho, Southern",
|
|
"spa": "Spanish; Latin",
|
|
"spa": "Spanish; Castilian",
|
|
"srd": "Sardinian",
|
|
"srn": "Sranan Tongo",
|
|
"srp": "Serbian",
|
|
"srr": "Serer",
|
|
"ssa": "Nilo-Saharan languages",
|
|
"ssw": "Swati",
|
|
"suk": "Sukuma",
|
|
"sun": "Sundanese",
|
|
"sus": "Susu",
|
|
"sux": "Sumerian",
|
|
"swa": "Swahili",
|
|
"swe": "Swedish",
|
|
"syc": "Classical Syriac",
|
|
"syr": "Syriac",
|
|
"tah": "Tahitian",
|
|
"tai": "Tai languages",
|
|
"tam": "Tamil",
|
|
"tat": "Tatar",
|
|
"tel": "Telugu",
|
|
"tem": "Timne",
|
|
"ter": "Tereno",
|
|
"tet": "Tetum",
|
|
"tgk": "Tajik",
|
|
"tgl": "Tagalog",
|
|
"tha": "Thai",
|
|
"tib": "Tibetan",
|
|
"tig": "Tigre",
|
|
"tir": "Tigrinya",
|
|
"tiv": "Tiv",
|
|
"tkl": "Tokelau",
|
|
"tlh": "Klingon; tlhIngan-Hol",
|
|
"tli": "Tlingit",
|
|
"tmh": "Tamashek",
|
|
"tog": "Tonga (Nyasa)",
|
|
"ton": "Tonga (Tonga Islands)",
|
|
"tpi": "Tok Pisin",
|
|
"tsi": "Tsimshian",
|
|
"tsn": "Tswana",
|
|
"tso": "Tsonga",
|
|
"tuk": "Turkmen",
|
|
"tum": "Tumbuka",
|
|
"tup": "Tupi languages",
|
|
"tur": "Turkish",
|
|
"tut": "Altaic languages",
|
|
"tvl": "Tuvalu",
|
|
"twi": "Twi",
|
|
"tyv": "Tuvinian",
|
|
"udm": "Udmurt",
|
|
"uga": "Ugaritic",
|
|
"uig": "Uighur; Uyghur",
|
|
"ukr": "Ukrainian",
|
|
"umb": "Umbundu",
|
|
"und": "Undetermined",
|
|
"urd": "Urdu",
|
|
"uzb": "Uzbek",
|
|
"vai": "Vai",
|
|
"ven": "Venda",
|
|
"vie": "Vietnamese",
|
|
"vol": "Volapük",
|
|
"vot": "Votic",
|
|
"wak": "Wakashan languages",
|
|
"wal": "Walamo",
|
|
"war": "Waray",
|
|
"was": "Washo",
|
|
"wel": "Welsh",
|
|
"wen": "Sorbian languages",
|
|
"wln": "Walloon",
|
|
"wol": "Wolof",
|
|
"xal": "Kalmyk; Oirat",
|
|
"xho": "Xhosa",
|
|
"yao": "Yao",
|
|
"yap": "Yapese",
|
|
"yid": "Yiddish",
|
|
"yor": "Yoruba",
|
|
"ypk": "Yupik languages",
|
|
"zap": "Zapotec",
|
|
"zbl": "Blissymbols; Blissymbolics; Bliss",
|
|
"zen": "Zenaga",
|
|
"zgh": "Standard Moroccan Tamazight",
|
|
"zha": "Zhuang; Chuang",
|
|
"znd": "Zande languages",
|
|
"zul": "Zulu",
|
|
"zun": "Zuni",
|
|
"zxx": "No linguistic content; Not applicable",
|
|
"zza": "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki"
|
|
}
|
|
end function
|