13ze commited on
Commit
35d63ba
·
verified ·
1 Parent(s): b122290

Upload 27 files

Browse files
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .idea/
2
+ _site/
3
+ gemfile.lock
BUILD.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Build Documentation
2
+ =
3
+
4
+ ### Setup
5
+
6
+ Get a copy of the repository
7
+
8
+ ```
9
+ git clone https://github.com/mattwright324/youtube-metadata.git
10
+ ```
11
+
12
+ Install Ruby (and gems jekyll and bundler) following the steps for your operating system
13
+
14
+ - https://jekyllrb.com/docs/installation/
15
+ - Ruby+Devkit default settings
16
+ - `gem install jekyll bundler`
17
+
18
+ In CMD or terminal, cd to the project directory you cloned to and run `bundle install` to install necessary gems
19
+ for github-pages in the project Gemfile (this may take some time).
20
+
21
+ ### Build and Run
22
+
23
+ In CMD or terminal, cd to the project directory you cloned to and run `bundle exec jekyll serve`
24
+ and you can view a live version at http://localhost:4000
25
+
26
+ Update the application with any editor you prefer (WebStorm, VSCode, Notepad++, etc.), Jekyll works such that edits to files
27
+ made while it is running will be picked up and published only needing you to refresh the browser instance to see them.
28
+
29
+ *Note: My YouTube API key provided in the application is restricted only to `https://mattw.io/*`, `http://localhost:4000/*`, and `http://127.0.0.1:4000/*`.
30
+ Access should work for all local development. Please do not abuse it such that it uses up the daily quota.*
31
+
32
+ ### Deployment note
33
+
34
+ If you are planning to deploy your own version of this application on another domain you will need your own YouTube and Maps API keys.
35
+
36
+ - https://console.cloud.google.com/apis/dashboard
37
+ - https://developers.google.com/youtube/v3/getting-started
38
+ - https://console.cloud.google.com/google/maps-apis/start
39
+
40
+ You can replace the YouTube API key in `youtube-api-v3.js` and replace the final line as such:
41
+
42
+ ```js
43
+ youtube.setDefaultKey('AIzaSy...');
44
+ ```
45
+
46
+ You can replace the Maps API key in `youtube-metadata.js` by searching for `maps.googleapis.com`.
47
+
48
+ It will also be important to note that the current YouTube API quota for new project keys is 10,000 units per day and may not be enough for your desired usage or if you plan to share your version.
49
+ My keys were lucky enough to be grandfathered in when the default quota limit was 1,000,000 units per day.
50
+ There is a process to request for more quota, however I have had not luck with it in the past.
51
+
52
+ - https://developers.google.com/youtube/v3/determine_quota_cost
53
+ - https://support.google.com/youtube/contact/yt_api_form
CONTRIBUTING.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Contributing Documentation
2
+ =
3
+
4
+ Contributions are certainly welcome and encouraged.
5
+
6
+ This application is hosted by [GitHub Pages](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/getting-started-with-github-pages)
7
+ meaning the `master` branch is what GitHub uses to deploy the application as found at https://mattw.io/youtube-metadata.
8
+
9
+ Pull requests should generally describe the changes being made or the issue being addressed.
10
+ Pull requests should be merged into the `develop` branch so that I may make any additional tweaks or adjustments before
11
+ merging into `master` for GitHub Pages to deploy.
12
+
13
+ Make the pull request from your own fork of the project.
14
+ It is recommended to base it off the `develop` branch.
15
+ However, depending how involved you would like to be, I can consider adding you to the main project if asked.
Gemfile ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ source 'https://rubygems.org'
2
+ gem 'github-pages'
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Matthew Wright
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
PRIVACY.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Privacy
2
+
3
+ This application uses YouTube API Services and Google Maps Services.
4
+
5
+ - http://www.google.com/policies/privacy
6
+
7
+ Basic user data is stored/collected by Google Analytics and Plausible and is being used for monitoring traffic and referrals.
8
+ Data collected by these services is not being shared with any other parties.
9
+ This application does not allow third parties to serve advertisements.
10
+
11
+ - http://www.google.com/policies/privacy
12
+ - https://plausible.io/privacy
13
+
14
+ Additional external calls are made to Filmot for archived video metadata.
15
+
16
+ - https://filmot.com/privacy
17
+
18
+ Additional external calls are made to my Heroku [cors-anywhere](https://github.com/Rob--W/cors-anywhere) proxy to resolve custom channel URLs to channel ids.
19
+
20
+ - https://www.heroku.com/policy/security
TERMS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Terms of Use
2
+
3
+ By using this application you are agreeing to be bound by YouTube's Terms of Service
4
+
5
+ - https://www.youtube.com/t/terms
_config.yml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ encoding : UTF-8
2
+ exclude : [.idea]
3
+ github : https://github.com/mattwright324/
_includes/format_share.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="container mb-15" style="margin-top: 1.25%">
2
+ <span>Accepted formats</span>
3
+ <ul id="formats" class="collapse show" style="padding-top:7px">
4
+ <li><span class="optional">https://www.</span>youtube.com/watch?v=<span class="orange">video_id</span></li>
5
+ <li><span class="optional">https://</span>youtube.com/shorts/<span class="orange">video_id</span></li>
6
+ <li><span class="optional">https://</span>youtu.be/<span class="orange">video_id</span></li>
7
+ <li><span class="optional">https://www.</span>youtube.com/playlist?list=<span class="orange">playlist_id</span></li>
8
+ <li><span class="optional">https://www.</span>youtube.com/channel/<span class="orange">channel_id</span></li>
9
+ <li><span class="optional">https://www.</span>youtube.com/user/<span class="orange">username</span></li>
10
+ <li><span class="optional">https://www.</span>youtube.com/@<span class="orange">channel_handle</span>
11
+ <li><span class="optional">https://www.</span>youtube.com/c/<span class="orange">custom_url</span></li>
12
+ <li><span class="optional">https://www.</span>youtube.com/<span class="orange">custom_url</span></li>
13
+ <li>
14
+ Also accepts direct ids:
15
+ <span class="orange">video_id</span>,
16
+ <span class="orange">playlist_id</span>,
17
+ <span class="orange">channel_id</span>
18
+ </li>
19
+ </ul>
20
+ </div>
_layouts/default.html ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="description"
6
+ content="Quickly gather all the metadata about a video, playlist, or channel from the YouTube API. Reverse image search thumbnails, geolocate in google maps, and translate ISO country and language codes.">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
8
+ <link rel="icon" href="./img/icon.png">
9
+ <title>{{ page.title }}</title>
10
+
11
+ <link rel="preconnect" href="//cdn.jsdelivr.net" crossorigin>
12
+
13
+ <!-- Dependencies -->
14
+ <link href="//cdn.datatables.net/1.11.3/css/dataTables.bootstrap5.min.css" rel="stylesheet">
15
+ <link href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.3.1/build/styles/stackoverflow-dark.min.css" rel="stylesheet"
16
+ id="highlightjs-theme">
17
+ <link href="//cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" rel="stylesheet">
18
+ <link href="//cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" rel="stylesheet">
19
+ <link href="./css/youtube-metadata.css?v={{ site.time | date_to_xmlschema }}" rel="stylesheet"/>
20
+ <script src="//code.jquery.com/jquery-3.6.0.min.js"></script>
21
+ <script src="//cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script>
22
+ <script src="//cdn.datatables.net/1.11.3/js/dataTables.bootstrap5.js"></script>
23
+ <script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.3.1/build/highlight.min.js"></script>
24
+ <script src="//cdn.jsdelivr.net/npm/apexcharts@3.32.0/dist/apexcharts.min.js"></script>
25
+ <script src="//cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"></script>
26
+ <script src="//cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
27
+ <script src="//cdn.jsdelivr.net/npm/bootstrap-multiselect@1.1.0/dist/js/bootstrap-multiselect.min.js"></script>
28
+ <script src="//cdn.jsdelivr.net/npm/clipboard@2.0.8/dist/clipboard.min.js"></script>
29
+ <script src="//cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>
30
+ <script src="//cdn.jsdelivr.net/npm/jszip-utils@0.1.0/dist/jszip-utils.min.js"></script>
31
+ <script src="//cdn.jsdelivr.net/npm/jszip@3.7.1/dist/jszip.min.js"></script>
32
+ <script src="//cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" type="text/javascript"></script>
33
+ <script src="//cdn.jsdelivr.net/npm/xregexp@5.1.0/xregexp-all.js"></script>
34
+ <script src="./js/randojs-2.0.0.js"></script>
35
+
36
+ <!-- App -->
37
+ <script src="./js/iso-639-translator.js?v={{ site.time | date_to_xmlschema }}" type="text/javascript"></script>
38
+ <script src="./js/iso-3166-translator.js?v={{ site.time | date_to_xmlschema }}" type="text/javascript"></script>
39
+ <script src="./js/bcp-47-translator.js?v={{ site.time | date_to_xmlschema }}" type="text/javascript"></script>
40
+ <script src="./js/yt-category-translator.js?v={{ site.time | date_to_xmlschema }}" type="text/javascript"></script>
41
+ <script src="./js/youtube-api-v3.js?v={{ site.time | date_to_xmlschema }}" type="text/javascript"></script>
42
+ <script src="./js/youtube-metadata-examples.js?v={{ site.time | date_to_xmlschema }}" type="text/javascript"></script>
43
+ <script src="./js/shared.js?v={{ site.time | date_to_xmlschema }}" type="text/javascript"></script>
44
+ <script src="./js/{{ page.script }}.js?v={{ site.time | date_to_xmlschema }}" type="text/javascript"></script>
45
+
46
+ <!-- Analytics -->
47
+ <script defer data-domain="mattw.io" src="https://plausible.io/js/plausible.js"></script>
48
+ </head>
49
+ <body>
50
+ <nav class="navbar navbar-expand-lg navbar-light mb-15" style="border-bottom: 1px solid rgba(127,127,127,0.1);">
51
+ <div class="container">
52
+ <div class="navbar-brand">
53
+ <img src="./img/icon.png" alt="" class="d-inline-block align-text-top">
54
+ <span style="padding-left: .75rem;">{{ page.title }}</span>
55
+ </div>
56
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
57
+ aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
58
+ <span class="navbar-toggler-icon"></span>
59
+ </button>
60
+ <div class="collapse navbar-collapse" id="navbarSupportedContent">
61
+ <ul class="navbar-nav me-auto mb-2 mb-lg-0">
62
+ <li class="nav-item">
63
+ <a class="nav-link" href="./">Normal</a>
64
+ </li>
65
+ <li class="nav-item">
66
+ <a class="nav-link" href="./bulk">Bulk</a>
67
+ </li>
68
+ </ul>
69
+ <form class="d-flex">
70
+ <div class="form-check form-switch">
71
+ <input class="form-check-input" type="checkbox" role="switch" id="darkMode" checked>
72
+ <label class="form-check-label" for="darkMode">Dark</label>
73
+ </div>
74
+ <script>
75
+ const switchDarkMode = document.querySelector("#darkMode");
76
+ switchDarkMode.onclick = function (e) {
77
+ darkmode.toggleDarkMode();
78
+ }
79
+ window.addEventListener('DOMContentLoaded', () => {
80
+ if (DarkMode.getColorScheme() === "dark") {
81
+ console.log(1)
82
+ switchDarkMode.setAttribute("checked", "checked");
83
+ } else {
84
+ console.log(2)
85
+ switchDarkMode.removeAttribute("checked");
86
+ }
87
+ });
88
+ </script>
89
+ </form>
90
+ </div>
91
+ </div>
92
+ </nav>
93
+ {{ content }}
94
+ <div class="ui container" style="padding-top:3%;padding-bottom:3%;color:darkgray;text-align:center">
95
+ <img src="https://developers.google.com/static/youtube/images/developed-with-youtube-sentence-case-light.png" style="width:200px!important"/>
96
+ <br>
97
+ mattwright324 | 2023 | <a href="/">home</a> | <a href="https://github.com/mattwright324">github</a> |
98
+ <a href="https://github.com/mattwright324/youtube-metadata">youtube-metadata</a>
99
+ <br>
100
+ <a href="https://github.com/mattwright324/youtube-metadata/blob/master/PRIVACY.md">privacy</a> |
101
+ <a href="https://github.com/mattwright324/youtube-metadata/blob/master/TERMS.md">terms of use</a> |
102
+ <a href="/">contact</a>
103
+ </div>
104
+ </body>
105
+ </html>
bulk.html ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ layout: default
3
+ title: MW Metadata Bulk
4
+ script: youtube-metadata-bulk
5
+ ---
6
+ <div class="container">
7
+ <p>
8
+ MW Metadata bulk grabs details about multiple YouTube videos, a playlist's videos, or a channel's public videos.
9
+ </p>
10
+ <p>
11
+ Submit a comma or whitespace separated list of multiple videos, playlists, or channels
12
+ </p>
13
+ <div class="input-group mb-3">
14
+ <input type="text" class="form-control" placeholder="https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
15
+ aria-label="https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw" aria-describedby="submit"
16
+ id="value" autocomplete="off">
17
+ <button class="btn btn-primary loading disabled" type="button" id="submit">
18
+ <span>Submit</span>
19
+ <span class="countdown"></span>
20
+ </button>
21
+ </div>
22
+ <div class="form-check">
23
+ <input class="form-check-input" type="checkbox" value="" id="createdPlaylists" autocomplete="off">
24
+ <label class="form-check-label" for="createdPlaylists">
25
+ Inspect videos in created playlists to find unlisted, private, and deleted videos (channels only)
26
+ </label>
27
+ </div>
28
+ </div>
29
+ {% include format_share.html %}
30
+ <div class="container mb-15">
31
+ <h2><span class="export">Export &</span> Share</h2>
32
+ <p class="export">Save this result as a zip file or load from a previous export. Drag and drop supported.</p>
33
+ <button class="btn btn-secondary export" id="export">
34
+ <span class="spinner"><span class="spinner-border spinner-border-sm" role="status"></span></span>
35
+ <span class="text"><i class="bi bi-download"></i>&nbsp;&nbsp;Export</span>
36
+ </button><span class="export">&nbsp;</span>
37
+ <button class="btn btn-secondary export" id="import" onclick="document.getElementById('importFileChooser').click()">
38
+ <span class="spinner"><span class="spinner-border spinner-border-sm" role="status"></span></span>
39
+ <span class="text"><i class="bi bi-upload"></i>&nbsp;&nbsp;Import</span>
40
+ <input id="importFileChooser" type="file" hidden/>
41
+ </button>
42
+ <div class="form-check export" style="margin:10px 0">
43
+ <input class="form-check-input" type="checkbox" value="" id="includeThumbs" autocomplete="off">
44
+ <label class="form-check-label" for="includeThumbs">
45
+ Include thumbnails on export (this may take a long time)
46
+ <span id="thumbProgress" class="orange"></span>
47
+ </label>
48
+ </div>
49
+ <p class="export">Contains file(s)</p>
50
+ <ul class="export">
51
+ <li>about.txt - Metadata about this result.</li>
52
+ <li>videos.json - Array of all raw video data.</li>
53
+ <li>channels.json - Array of all raw channel data.</li>
54
+ <li>playlists.json - Array of all raw playlist data.</li>
55
+ <li>unavailable.json - Basic data for unavailable videos.</li>
56
+ <li>✱.csv - Data from each table section below.</li>
57
+ <li>thumbs/✱.png - Thumbs if available.</li>
58
+ </ul>
59
+ <p class="export"></p>
60
+ <p>Share this result:</p>
61
+ <div class="input-group" style="max-width: 226px;">
62
+ <input type="text" class="form-control" aria-describedby="copy" id="shareLink" autocomplete="off">
63
+ <button class="btn btn-primary clipboard" type="button" data-clipboard-target="#shareLink"><i
64
+ class="bi bi-clipboard"></i></button>
65
+ </div>
66
+ <p style="margin-top: 10px">
67
+ <a target="_blank" href="https://github.com/mattwright324/youtube-metadata/discussions/150">
68
+ What happened to export?
69
+ </a>
70
+ </p>
71
+ </div>
72
+ <div class="container mb-15">
73
+ <h2>Videos</h2>
74
+ <p>
75
+ List of all videos, playlist videos, or channel public uploads.
76
+ Customize columns to show more or less data as needed.
77
+ </p>
78
+ <div class="progress" style="margin-bottom:5px">
79
+ <div id="progressBar" class="progress-bar" role="progressbar" style="" aria-valuenow="0" aria-valuemin="0"
80
+ aria-valuemax="100">
81
+ <div class="label"></div>
82
+ </div>
83
+ </div>
84
+ <div id="progressText" style="width: 100%;text-align: center;">Idle</div>
85
+ <br>
86
+ <label for="column-options">Select column(s)</label>
87
+ <select id="column-options" multiple="multiple" hidden></select>
88
+ </div>
89
+ <div class="container-fluid mb-15">
90
+ <table id="videosTable" class="table table-striped table-hover" style="width:100%"></table>
91
+ </div>
92
+ <div class="container mb-15">
93
+ <h2>Tags</h2>
94
+ <p>Aggregate total tag counts on found videos.</p>
95
+ <table id="tagsTable" class="table table-striped table-hover" style="width:100%"></table>
96
+ </div>
97
+ <div class="container mb-15">
98
+ <h2>Geotags</h2>
99
+ <p>
100
+ Aggregate total geotag counts on found videos.
101
+ Want to visualize this data in a map?
102
+ Export this result and import the zip into
103
+ <a href="https://mattw.io/youtube-geofind" target="_blank">YouTube Geofind</a>!</p>
104
+ </p>
105
+ <table id="geotagsTable" class="table table-striped table-hover" style="width:100%"></table>
106
+ </div>
107
+ <div class="container mb-15">
108
+ <h2>Links</h2>
109
+ <p>
110
+ Aggregate total link counts in found video descriptions.
111
+ Be careful visiting what may show up here, by default results will not be hyperlinked for safety.
112
+ </p>
113
+ <div class="form-check mb-15">
114
+ <input class="form-check-input" type="checkbox" value="" id="makeHyperlinks" autocomplete="off"
115
+ onchange="bulk.toggleLinksColumn(0);bulk.toggleLinksColumn(1)">
116
+ <label class="form-check-label" for="makeHyperlinks">
117
+ Make them hyperlinks anyways
118
+ </label>
119
+ </div>
120
+ <table id="linksTable" class="table table-striped table-hover" style="width:100%"></table>
121
+ </div>
122
+ <div class="container mb-15">
123
+ <h2>Upload Frequency</h2>
124
+ <p>
125
+ Aggregate uploads frequency by hour and day.
126
+ Transformable to any timezone
127
+ (<a target="_blank" href="https://upload.wikimedia.org/wikipedia/commons/8/88/World_Time_Zones_Map.png">map</a>)
128
+ and filterable by year when activity occurred.
129
+ Guesstimate region using typical sleep/activity patterns, see change in behavior over time,
130
+ or see typical upload times in your own timezone.
131
+ </p>
132
+ <div class="row" style="margin: 0 0 1.5em 0">
133
+ <select id="offset" class="form-select" style="width:auto;margin-right:10px" autocomplete="off">
134
+ <option value="-1200">UTC −12:00</option>
135
+ <option value="-1100">UTC −11:00</option>
136
+ <option value="-1000">UTC −10:00</option>
137
+ <option value="-0930">UTC −09:30</option>
138
+ <option value="-0900">UTC −09:00</option>
139
+ <option value="-0800">UTC −08:00</option>
140
+ <option value="-0700">UTC −07:00</option>
141
+ <option value="-0600">UTC −06:00</option>
142
+ <option value="-0500">UTC −05:00</option>
143
+ <option value="-0400">UTC −04:00</option>
144
+ <option value="-0330">UTC −03:30</option>
145
+ <option value="-0300">UTC −03:00</option>
146
+ <option value="-0200">UTC −02:00</option>
147
+ <option value="-0100">UTC −01:00</option>
148
+ <option value="+0000" selected>UTC ±00:00</option>
149
+ <option value="+0100">UTC +01:00</option>
150
+ <option value="+0200">UTC +02:00</option>
151
+ <option value="+0300">UTC +03:00</option>
152
+ <option value="+0330">UTC +03:30</option>
153
+ <option value="+0400">UTC +04:00</option>
154
+ <option value="+0430">UTC +04:30</option>
155
+ <option value="+0500">UTC +05:00</option>
156
+ <option value="+0530">UTC +05:30</option>
157
+ <option value="+0545">UTC +05:45</option>
158
+ <option value="+0600">UTC +06:00</option>
159
+ <option value="+0630">UTC +06:30</option>
160
+ <option value="+0700">UTC +07:00</option>
161
+ <option value="+0800">UTC +08:00</option>
162
+ <option value="+0845">UTC +08:45</option>
163
+ <option value="+0900">UTC +09:00</option>
164
+ <option value="+0930">UTC +09:30</option>
165
+ <option value="+1000">UTC +10:00</option>
166
+ <option value="+1030">UTC +10:30</option>
167
+ <option value="+1100">UTC +11:00</option>
168
+ <option value="+1200">UTC +12:00</option>
169
+ <option value="+1245">UTC +12:45</option>
170
+ <option value="+1300">UTC +13:00</option>
171
+ <option value="+1345">UTC +13:45</option>
172
+ <option value="+1400">UTC +14:00</option>
173
+ </select>
174
+ <select id="year" class="form-select" style="width:auto" autocomplete="off">
175
+ <option value="" selected>All years</option>
176
+ </select>
177
+ </div>
178
+ <div id="uploadFrequency"></div>
179
+ </div>
180
+ <div class="container mb-15">
181
+ <h2>Unavailable</h2>
182
+ <p>
183
+ Videos that are unavailable from channels (created playlists on), playlists, or videos.
184
+ They are either <a target="_blank" href="https://github.com/mattwright324/youtube-metadata/wiki/Deleted-and-Private-Videos">deleted or made private</a>.
185
+ Consider searching for user-made playlists to find more unavailable videos.
186
+ </p>
187
+ <div id="unavailable">
188
+ <div class="input-group mb-3" style="align-items: center;">
189
+ <button class="btn btn-secondary loading disabled" type="button" id="checkUnavailable" style="margin-right: 10px">
190
+ <span>Check Unavailable</span>
191
+ <span class="countdown"></span>
192
+ </button>
193
+ <span>Check Filmot for any archived metadata for unavailable videos.</span>
194
+ </div>
195
+ <div class="progress" style="margin-bottom:5px">
196
+ <div id="unavailableProgressBar" class="progress-bar" role="progressbar" style="" aria-valuenow="0" aria-valuemin="0"
197
+ aria-valuemax="100">
198
+ <div class="label"></div>
199
+ </div>
200
+ </div>
201
+ <div id="unavailableProgressText" style="width: 100%;text-align: center;">Idle</div>
202
+ <br>
203
+ </div>
204
+ <label for="unavailable-columns">Select column(s)</label>
205
+ <select id="unavailable-columns" multiple="multiple" hidden></select>
206
+ </div>
207
+ <div class="container-fluid mb-15">
208
+ <table id="unavailableTable" class="table table-striped table-hover" style="width:100%"></table>
209
+ </div>
210
+ <div class="container mb-15">
211
+ <h2>Other</h2>
212
+ <p>Other potentially useful aggregate data on found videos.</p>
213
+ <table id="otherTable" class="table table-striped table-hover" style="width:100%"></table>
214
+ </div>
css/youtube-metadata.css ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ background-repeat: repeat;
3
+ font-weight: 350 !important;
4
+ }
5
+
6
+ a[href] {
7
+ text-decoration: none;
8
+ }
9
+
10
+ ul {
11
+ line-height: 20px;
12
+ }
13
+
14
+ code {
15
+ max-height: 180px;
16
+ }
17
+
18
+ .mb-15 {
19
+ margin-bottom: 1.5%;
20
+ }
21
+
22
+ p.mb-15 {
23
+ margin-bottom: 0.75%;
24
+ }
25
+
26
+ .mt-15 {
27
+ margin-top: 1.5%;
28
+ }
29
+
30
+ .pt-15 {
31
+ padding-top: 1.5%;
32
+ }
33
+
34
+ .navbar-brand {
35
+ font-size: 1.75rem!important;
36
+ }
37
+
38
+ .navbar {
39
+ font-size: 1.5rem!important;
40
+ }
41
+
42
+ .orange {
43
+ color: orange;
44
+ }
45
+
46
+ .red {
47
+ color: red;
48
+ }
49
+
50
+ .green {
51
+ color: green;
52
+ }
53
+
54
+ .optional {
55
+ opacity: 0.75;
56
+ }
57
+
58
+ .section-header {
59
+ font-size: 1.5em;
60
+ margin: 0 0 0.3em;
61
+ }
62
+
63
+ .unknown {
64
+ color: mediumpurple;
65
+ }
66
+
67
+ .good {
68
+ color: mediumseagreen;
69
+ }
70
+
71
+ .bad, .gray {
72
+ color: lightgray;
73
+ }
74
+
75
+ .profile {
76
+ width: 100px;
77
+ height: 100px;
78
+ }
79
+
80
+ img {
81
+ width: 100%;
82
+ max-width: 500px;
83
+ height: auto;
84
+ }
85
+
86
+ .navbar-brand img {
87
+ width: 40px !important;
88
+ height: 40px !important;
89
+ }
90
+
91
+ .section-header span {
92
+ margin-left: 8px;
93
+ }
94
+
95
+ #export, #import {
96
+ width: 100px;
97
+ }
98
+
99
+ button.loading .text {
100
+ display: none;
101
+ }
102
+
103
+ .dropdown-toggle::after {
104
+ content: none
105
+ }
106
+
107
+ .progress {
108
+ height: 2em;
109
+ }
110
+
111
+ .tag {
112
+ background-color: lightskyblue;
113
+ padding: 2px 5px;
114
+ border-radius: 5px;
115
+ margin-right: 4px;
116
+ margin-bottom: 3px;
117
+ display: inline-block;
118
+ }
119
+
120
+ .comma {
121
+ color: transparent;
122
+ width: 0;
123
+ display: inline-block;
124
+ }
125
+
126
+ button .countdown, button .spinner {
127
+ display: none;
128
+ }
129
+
130
+ button.loading .countdown, button.loading .spinner {
131
+ display: initial;
132
+ }
133
+
134
+ .progress-bar.error {
135
+ background-color: #e67e22;
136
+ }
137
+
138
+ html.dark .progress-bar.error {
139
+ background-color: #ca6f1e;
140
+ }
141
+
142
+ /* Dark mode tweaks */
143
+ html.dark .tag {
144
+ background-color: royalblue;
145
+ }
146
+
147
+ html.dark .btn-primary {
148
+ background-color: #0d6efd;
149
+ border-color: #0d6efd;
150
+ }
151
+
152
+ html.dark a {
153
+ color: lightskyblue;
154
+ }
155
+
156
+ html.dark .progress-bar {
157
+ color: #fff;
158
+ }
159
+
160
+ .wayback-cdx-older {
161
+ filter: brightness(0.75);
162
+ }
163
+
164
+ .export {
165
+ display: none;
166
+ }
167
+
168
+ /*
169
+ Use Stylus extension!
170
+
171
+ https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne
172
+ https://addons.mozilla.org/en-US/firefox/addon/styl-us/
173
+
174
+ .export {
175
+ display: revert!important;
176
+ }
177
+ */
img/icon.pdn ADDED
Binary file (14.8 kB). View file
 
img/icon.png ADDED
img/metadata.png ADDED
img/readme.png ADDED
index.html CHANGED
@@ -1,19 +1,142 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ layout: default
3
+ title: MW Metadata
4
+ script: youtube-metadata
5
+ ---
6
+ <div class="container">
7
+ <p>
8
+ MW Metadata normal grabs singular details about a YouTube video and its uploader, playlist and its creator, or channel.
9
+ </p>
10
+ <p>
11
+ Submit a link or id to a video, playlist, or channel
12
+ </p>
13
+ <div class="input-group">
14
+ <input type="text" class="form-control" placeholder="https://youtu.be/dQw4w9WgXcQ" autocomplete="off"
15
+ aria-label="https://youtu.be/dQw4w9WgXcQ" aria-describedby="submit" id="value">
16
+ <button class="btn btn-primary loading disabled" type="button" id="submit" >
17
+ <span>Submit</span>
18
+ <span class="countdown"></span>
19
+ </button>
20
+ </div>
21
+ </div>
22
+ {% include format_share.html %}
23
+ <div class="container mb-15">
24
+ <h2><span class="export">Export &</span> Share</h2>
25
+ <p class="export">Save this result as a zip file or load from a previous export. Drag and drop supported.</p>
26
+ <button class="btn btn-secondary export" id="export">
27
+ <span class="spinner"><span class="spinner-border spinner-border-sm" role="status"></span></span>
28
+ <span class="text"><i class="bi bi-download"></i>&nbsp;&nbsp;Export</span>
29
+ </button><span class="export">&nbsp;</span>
30
+ <button class="btn btn-secondary export" id="import" onclick="document.getElementById('importFileChooser').click()">
31
+ <span class="spinner"><span class="spinner-border spinner-border-sm" role="status"></span></span>
32
+ <span class="text"><i class="bi bi-upload"></i>&nbsp;&nbsp;Import</span>
33
+ <input id="importFileChooser" type="file" hidden/>
34
+ </button>
35
+ <p class="export"></p>
36
+ <p class="export">Contains file(s)</p>
37
+ <ul class="export">
38
+ <li>about.txt - Metadata about this result.</li>
39
+ <li>video.json - Raw video data.</li>
40
+ <li>playlist.json - Raw playlist data.</li>
41
+ <li>channel.json - Raw channel data.</li>
42
+ <li>filmot.json - Archive data if retrieved.</li>
43
+ <li>✱-thumb.png - Thumbs if available.</li>
44
+ </ul>
45
+ <p class="export"></p>
46
+ <p>Share this result:</p>
47
+ <div class="input-group" style="max-width: 226px;">
48
+ <input type="text" class="form-control" aria-describedby="copy" id="shareLink" autocomplete="off">
49
+ <button class="btn btn-primary clipboard" type="button" data-clipboard-target="#shareLink"><i class="bi bi-clipboard"></i></button>
50
+ </div>
51
+ <p style="margin-top: 10px">
52
+ <a target="_blank" href="https://github.com/mattwright324/youtube-metadata/discussions/150">
53
+ What happened to export?
54
+ </a>
55
+ </p>
56
+ </div>
57
+ <div class="container mb-15" id="video">
58
+ <h2>Video</h2>
59
+ <p>The video submitted.
60
+ Click <a href="https://developers.google.com/youtube/v3/docs/videos#properties" target="_blank">here</a>
61
+ to see detailed property descriptions.
62
+ </p>
63
+ <div id="video-section"></div>
64
+
65
+ <div class='section-header unknown'><i class='bi bi-question-circle-fill'></i><span>Thumbnails</span></div>
66
+ <p>Reverse image search all four thumbnail images.</p>
67
+ <div id="thumbnails" class="row row-cols-4"></div>
68
+
69
+ <div class='section-header unknown'><i class='bi bi-question-circle-fill'></i><span>More</span></div>
70
+ <p>Check other resources for details or archival.</p>
71
+ <div id="video-more" style="padding-top:0"></div>
72
+ </div>
73
+ <div class="container mb-15" id="playlist">
74
+ <h2>Playlist</h2>
75
+ <p>The playlist submitted.
76
+ Click <a href="https://developers.google.com/youtube/v3/docs/playlists#properties"
77
+ target="_blank">here</a> to see detailed property descriptions.
78
+ </p>
79
+ <div id="playlist-section"></div>
80
+
81
+ <div class='section-header unknown'><i class='bi bi-question-circle-fill'></i><span>More</span></div>
82
+ <p>Check other resources for details or archival.</p>
83
+ <div id="playlist-more" style="padding-top:0"></div>
84
+ </div>
85
+ <div class="container mb-15" id="channel">
86
+ <h2>Channel</h2>
87
+ <p>The video author, playlist creator, or channel submitted.
88
+ Click <a href="https://developers.google.com/youtube/v3/docs/channels#properties" target="_blank">here</a>
89
+ to see detailed property descriptions.
90
+ </p>
91
+ <div id="channel-section"></div>
92
+
93
+ <div class='section-header unknown'><i class='bi bi-question-circle-fill'></i><span>More</span></div>
94
+ <p>Check other resources for details or archival.</p>
95
+ <div id="channel-more" style="padding-top:0"></div>
96
+ </div>
97
+ <div class="container mb-15" id="unknown" style="display: none">
98
+ <h2>Unknown</h2>
99
+ <p id="reason" class="mb-15"></p>
100
+ <div id="reason-append"></div>
101
+ </div>
102
+ <div class="container mb-15" id="quota" style="display: none">
103
+ <h2>Quota Exceeded</h2>
104
+ <p class="mb-15">
105
+ The daily YouTube API quota has been reached for this application.
106
+ Quota resets at <span class="orange">midnight Pacific Time (PT)</span> as per the Google Developers Console.
107
+ </p>
108
+ <p class="mb-15">
109
+ See more detail here with
110
+ <a href="https://github.com/mattwright324/youtube-metadata/issues/3" target="_blank">issue #3</a>.
111
+ </p>
112
+ <p class="mb-15">
113
+ Since the API is continually reaching its quota due to someone, you can use your own API key with this
114
+ <a href="https://github.com/mattwright324/youtube-metadata/issues/3#issuecomment-1532385453" target="_blank">
115
+ Tampermonkey userscript</a>.
116
+ <a href="https://developers.google.com/youtube/v3/getting-started#intro" target="_blank">
117
+ Here</a> to create your own key.
118
+ </p>
119
+ <div id="quota-reason-append"></div>
120
+ </div>
121
+ <div class="container mb-15" id="filmot" style="display: none">
122
+ <h2>Filmot Archive</h2>
123
+ <p class="mb-15">
124
+ Data retrieved from Filmot about this unavailable video.
125
+ </p>
126
+ <div id="filmot-append"></div>
127
+ </div>
128
+ <div class="container mb-15" id="wayback" style="display: none">
129
+ <h2>Wayback CDX</h2>
130
+ <p class="mb-15">
131
+ Potential metadata returned from Wayback CDX API. Searches for video and video storyboard thumbnails.
132
+ </p>
133
+ <button class="btn btn-secondary" id="wayback-check" style="margin-bottom:8px">Check CDX</button>
134
+ <div id="wayback-show-older" class="form-check" style="margin-bottom:8px;display:none">
135
+ <input class="form-check-input" type="checkbox" value="" id="checkHideOlder" checked>
136
+ <label class="form-check-label" for="checkHideOlder">
137
+ Hide older versions of the same link (<span class="older-count">0</span> older)
138
+ </label>
139
+ </div>
140
+ <div id="wayback-append"></div>
141
+ <button class="btn btn-secondary" id="wayback-copy" style="margin-bottom:8px;display:none">Copy visible links</button>
142
+ </div>
js/bcp-47-translator.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * BCP 47 is a combination of country code ISO 3166 and language code ISO 639.
3
+ *
4
+ * This translates both codes.
5
+ */
6
+ const bcp47 = (function (iso3166, iso639) {
7
+ 'use strict';
8
+
9
+ return {
10
+ lookup: function (code) {
11
+ if (!code) {
12
+ return;
13
+ }
14
+
15
+ const parts = String(code).split(/[-|_]/);
16
+ return {
17
+ language: iso639.lookup(parts[0]) || iso639.lookup(parts[1]),
18
+ country: iso3166.lookup(parts[0]) || iso3166.lookup(parts[1])
19
+ }
20
+ }
21
+ }
22
+ }(iso3166, iso639));
js/iso-3166-translator.js ADDED
@@ -0,0 +1,1275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Implementation to quickly translate ISO 3166-1 two and three letter and digit country codes.
3
+ *
4
+ * @link https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
5
+ */
6
+ const iso3166 = (function () {
7
+ 'use strict';
8
+
9
+ const codes = [
10
+ {
11
+ "name": "Afghanistan",
12
+ "numeric3": "004",
13
+ "alpha2": "AF"
14
+ },
15
+ {
16
+ "name": "Åland Islands",
17
+ "numeric3": "248",
18
+ "alpha2": "AX"
19
+ },
20
+ {
21
+ "name": "Albania",
22
+ "numeric3": "008",
23
+ "alpha2": "AL"
24
+ },
25
+ {
26
+ "name": "Algeria",
27
+ "numeric3": "012",
28
+ "alpha2": "DZ"
29
+ },
30
+ {
31
+ "name": "American Samoa",
32
+ "numeric3": "016",
33
+ "alpha2": "AS"
34
+ },
35
+ {
36
+ "name": "Andorra",
37
+ "numeric3": "020",
38
+ "alpha2": "AD"
39
+ },
40
+ {
41
+ "name": "Angola",
42
+ "numeric3": "024",
43
+ "alpha2": "AO"
44
+ },
45
+ {
46
+ "name": "Anguilla",
47
+ "numeric3": "660",
48
+ "alpha2": "AI"
49
+ },
50
+ {
51
+ "name": "Antarctica",
52
+ "numeric3": "010",
53
+ "alpha2": "AQ"
54
+ },
55
+ {
56
+ "name": "Antigua and Barbuda",
57
+ "numeric3": "028",
58
+ "alpha2": "AG"
59
+ },
60
+ {
61
+ "name": "Argentina",
62
+ "numeric3": "032",
63
+ "alpha2": "AR"
64
+ },
65
+ {
66
+ "name": "Armenia",
67
+ "numeric3": "051",
68
+ "alpha2": "AM"
69
+ },
70
+ {
71
+ "name": "Aruba",
72
+ "numeric3": "533",
73
+ "alpha2": "AW"
74
+ },
75
+ {
76
+ "name": "Australia",
77
+ "numeric3": "036",
78
+ "alpha2": "AU"
79
+ },
80
+ {
81
+ "name": "Austria",
82
+ "numeric3": "040",
83
+ "alpha2": "AT"
84
+ },
85
+ {
86
+ "name": "Azerbaijan",
87
+ "numeric3": "031",
88
+ "alpha2": "AZ"
89
+ },
90
+ {
91
+ "name": "Bahamas (the)",
92
+ "numeric3": "044",
93
+ "alpha2": "BS"
94
+ },
95
+ {
96
+ "name": "Bahrain",
97
+ "numeric3": "048",
98
+ "alpha2": "BH"
99
+ },
100
+ {
101
+ "name": "Bangladesh",
102
+ "numeric3": "050",
103
+ "alpha2": "BD"
104
+ },
105
+ {
106
+ "name": "Barbados",
107
+ "numeric3": "052",
108
+ "alpha2": "BB"
109
+ },
110
+ {
111
+ "name": "Belarus",
112
+ "numeric3": "112",
113
+ "alpha2": "BY"
114
+ },
115
+ {
116
+ "name": "Belgium",
117
+ "numeric3": "056",
118
+ "alpha2": "BE"
119
+ },
120
+ {
121
+ "name": "Belize",
122
+ "numeric3": "084",
123
+ "alpha2": "BZ"
124
+ },
125
+ {
126
+ "name": "Benin",
127
+ "numeric3": "204",
128
+ "alpha2": "BJ"
129
+ },
130
+ {
131
+ "name": "Bermuda",
132
+ "numeric3": "060",
133
+ "alpha2": "BM"
134
+ },
135
+ {
136
+ "name": "Bhutan",
137
+ "numeric3": "064",
138
+ "alpha2": "BT"
139
+ },
140
+ {
141
+ "name": "Bolivia (Plurinational State of)",
142
+ "numeric3": "068",
143
+ "alpha2": "BO"
144
+ },
145
+ {
146
+ "name": "Bonaire Sint Eustatius Saba",
147
+ "numeric3": "535",
148
+ "alpha2": "BQ"
149
+ },
150
+ {
151
+ "name": "Bosnia and Herzegovina",
152
+ "numeric3": "070",
153
+ "alpha2": "BA"
154
+ },
155
+ {
156
+ "name": "Botswana",
157
+ "numeric3": "072",
158
+ "alpha2": "BW"
159
+ },
160
+ {
161
+ "name": "Bouvet Island",
162
+ "numeric3": "074",
163
+ "alpha2": "BV"
164
+ },
165
+ {
166
+ "name": "Brazil",
167
+ "numeric3": "076",
168
+ "alpha2": "BR"
169
+ },
170
+ {
171
+ "name": "British Indian Ocean Territory (the)",
172
+ "numeric3": "086",
173
+ "alpha2": "IO"
174
+ },
175
+ {
176
+ "name": "Brunei Darussalam",
177
+ "numeric3": "096",
178
+ "alpha2": "BN"
179
+ },
180
+ {
181
+ "name": "Bulgaria",
182
+ "numeric3": "100",
183
+ "alpha2": "BG"
184
+ },
185
+ {
186
+ "name": "Burkina Faso",
187
+ "numeric3": "854",
188
+ "alpha2": "BF"
189
+ },
190
+ {
191
+ "name": "Burundi",
192
+ "numeric3": "108",
193
+ "alpha2": "BI"
194
+ },
195
+ {
196
+ "name": "Cabo Verde",
197
+ "numeric3": "132",
198
+ "alpha2": "CV"
199
+ },
200
+ {
201
+ "name": "Cambodia",
202
+ "numeric3": "116",
203
+ "alpha2": "KH"
204
+ },
205
+ {
206
+ "name": "Cameroon",
207
+ "numeric3": "120",
208
+ "alpha2": "CM"
209
+ },
210
+ {
211
+ "name": "Canada",
212
+ "numeric3": "124",
213
+ "alpha2": "CA"
214
+ },
215
+ {
216
+ "name": "Cayman Islands (the)",
217
+ "numeric3": "136",
218
+ "alpha2": "KY"
219
+ },
220
+ {
221
+ "name": "Central African Republic (the)",
222
+ "numeric3": "140",
223
+ "alpha2": "CF"
224
+ },
225
+ {
226
+ "name": "Chad",
227
+ "numeric3": "148",
228
+ "alpha2": "TD"
229
+ },
230
+ {
231
+ "name": "Chile",
232
+ "numeric3": "152",
233
+ "alpha2": "CL"
234
+ },
235
+ {
236
+ "name": "China",
237
+ "numeric3": "156",
238
+ "alpha2": "CN"
239
+ },
240
+ {
241
+ "name": "Christmas Island",
242
+ "numeric3": "162",
243
+ "alpha2": "CX"
244
+ },
245
+ {
246
+ "name": "Cocos (Keeling) Islands (the)",
247
+ "numeric3": "166",
248
+ "alpha2": "CC"
249
+ },
250
+ {
251
+ "name": "Colombia",
252
+ "numeric3": "170",
253
+ "alpha2": "CO"
254
+ },
255
+ {
256
+ "name": "Comoros (the)",
257
+ "numeric3": "174",
258
+ "alpha2": "KM"
259
+ },
260
+ {
261
+ "name": "Congo (the Democratic Republic of the)",
262
+ "numeric3": "180",
263
+ "alpha2": "CD"
264
+ },
265
+ {
266
+ "name": "Congo (the)",
267
+ "numeric3": "178",
268
+ "alpha2": "CG"
269
+ },
270
+ {
271
+ "name": "Cook Islands (the)",
272
+ "numeric3": "184",
273
+ "alpha2": "CK"
274
+ },
275
+ {
276
+ "name": "Costa Rica",
277
+ "numeric3": "188",
278
+ "alpha2": "CR"
279
+ },
280
+ {
281
+ "name": "Côte d'Ivoire",
282
+ "numeric3": "384",
283
+ "alpha2": "CI"
284
+ },
285
+ {
286
+ "name": "Croatia",
287
+ "numeric3": "191",
288
+ "alpha2": "HR"
289
+ },
290
+ {
291
+ "name": "Cuba",
292
+ "numeric3": "192",
293
+ "alpha2": "CU"
294
+ },
295
+ {
296
+ "name": "Curaçao",
297
+ "numeric3": "531",
298
+ "alpha2": "CW"
299
+ },
300
+ {
301
+ "name": "Cyprus",
302
+ "numeric3": "196",
303
+ "alpha2": "CY"
304
+ },
305
+ {
306
+ "name": "Czechia",
307
+ "numeric3": "203",
308
+ "alpha2": "CZ"
309
+ },
310
+ {
311
+ "name": "Denmark",
312
+ "numeric3": "208",
313
+ "alpha2": "DK"
314
+ },
315
+ {
316
+ "name": "Djibouti",
317
+ "numeric3": "262",
318
+ "alpha2": "DJ"
319
+ },
320
+ {
321
+ "name": "Dominica",
322
+ "numeric3": "212",
323
+ "alpha2": "DM"
324
+ },
325
+ {
326
+ "name": "Dominican Republic (the)",
327
+ "numeric3": "214",
328
+ "alpha2": "DO"
329
+ },
330
+ {
331
+ "name": "Ecuador",
332
+ "numeric3": "218",
333
+ "alpha2": "EC"
334
+ },
335
+ {
336
+ "name": "Egypt",
337
+ "numeric3": "818",
338
+ "alpha2": "EG"
339
+ },
340
+ {
341
+ "name": "El Salvador",
342
+ "numeric3": "222",
343
+ "alpha2": "SV"
344
+ },
345
+ {
346
+ "name": "Equatorial Guinea",
347
+ "numeric3": "226",
348
+ "alpha2": "GQ"
349
+ },
350
+ {
351
+ "name": "Eritrea",
352
+ "numeric3": "232",
353
+ "alpha2": "ER"
354
+ },
355
+ {
356
+ "name": "Estonia",
357
+ "numeric3": "233",
358
+ "alpha2": "EE"
359
+ },
360
+ {
361
+ "name": "Eswatini",
362
+ "numeric3": "748",
363
+ "alpha2": "SZ"
364
+ },
365
+ {
366
+ "name": "Ethiopia",
367
+ "numeric3": "231",
368
+ "alpha2": "ET"
369
+ },
370
+ {
371
+ "name": "Falkland Islands (the) Malvinas",
372
+ "numeric3": "238",
373
+ "alpha2": "FK"
374
+ },
375
+ {
376
+ "name": "Faroe Islands (the)",
377
+ "numeric3": "234",
378
+ "alpha2": "FO"
379
+ },
380
+ {
381
+ "name": "Fiji",
382
+ "numeric3": "242",
383
+ "alpha2": "FJ"
384
+ },
385
+ {
386
+ "name": "Finland",
387
+ "numeric3": "246",
388
+ "alpha2": "FI"
389
+ },
390
+ {
391
+ "name": "France",
392
+ "numeric3": "250",
393
+ "alpha2": "FR"
394
+ },
395
+ {
396
+ "name": "French Guiana",
397
+ "numeric3": "254",
398
+ "alpha2": "GF"
399
+ },
400
+ {
401
+ "name": "French Polynesia",
402
+ "numeric3": "258",
403
+ "alpha2": "PF"
404
+ },
405
+ {
406
+ "name": "French Southern Territories (the)",
407
+ "numeric3": "260",
408
+ "alpha2": "TF"
409
+ },
410
+ {
411
+ "name": "Gabon",
412
+ "numeric3": "266",
413
+ "alpha2": "GA"
414
+ },
415
+ {
416
+ "name": "Gambia (the)",
417
+ "numeric3": "270",
418
+ "alpha2": "GM"
419
+ },
420
+ {
421
+ "name": "Georgia",
422
+ "numeric3": "268",
423
+ "alpha2": "GE"
424
+ },
425
+ {
426
+ "name": "Germany",
427
+ "numeric3": "276",
428
+ "alpha2": "DE"
429
+ },
430
+ {
431
+ "name": "Ghana",
432
+ "numeric3": "288",
433
+ "alpha2": "GH"
434
+ },
435
+ {
436
+ "name": "Gibraltar",
437
+ "numeric3": "292",
438
+ "alpha2": "GI"
439
+ },
440
+ {
441
+ "name": "Greece",
442
+ "numeric3": "300",
443
+ "alpha2": "GR"
444
+ },
445
+ {
446
+ "name": "Greenland",
447
+ "numeric3": "304",
448
+ "alpha2": "GL"
449
+ },
450
+ {
451
+ "name": "Grenada",
452
+ "numeric3": "308",
453
+ "alpha2": "GD"
454
+ },
455
+ {
456
+ "name": "Guadeloupe",
457
+ "numeric3": "312",
458
+ "alpha2": "GP"
459
+ },
460
+ {
461
+ "name": "Guam",
462
+ "numeric3": "316",
463
+ "alpha2": "GU"
464
+ },
465
+ {
466
+ "name": "Guatemala",
467
+ "numeric3": "320",
468
+ "alpha2": "GT"
469
+ },
470
+ {
471
+ "name": "Guernsey",
472
+ "numeric3": "831",
473
+ "alpha2": "GG"
474
+ },
475
+ {
476
+ "name": "Guinea",
477
+ "numeric3": "324",
478
+ "alpha2": "GN"
479
+ },
480
+ {
481
+ "name": "Guinea-Bissau",
482
+ "numeric3": "624",
483
+ "alpha2": "GW"
484
+ },
485
+ {
486
+ "name": "Guyana",
487
+ "numeric3": "328",
488
+ "alpha2": "GY"
489
+ },
490
+ {
491
+ "name": "Haiti",
492
+ "numeric3": "332",
493
+ "alpha2": "HT"
494
+ },
495
+ {
496
+ "name": "Heard Island and McDonald Islands",
497
+ "numeric3": "334",
498
+ "alpha2": "HM"
499
+ },
500
+ {
501
+ "name": "Holy See (the)",
502
+ "numeric3": "336",
503
+ "alpha2": "VA"
504
+ },
505
+ {
506
+ "name": "Honduras",
507
+ "numeric3": "340",
508
+ "alpha2": "HN"
509
+ },
510
+ {
511
+ "name": "Hong Kong",
512
+ "numeric3": "344",
513
+ "alpha2": "HK"
514
+ },
515
+ {
516
+ "name": "Hungary",
517
+ "numeric3": "348",
518
+ "alpha2": "HU"
519
+ },
520
+ {
521
+ "name": "Iceland",
522
+ "numeric3": "352",
523
+ "alpha2": "IS"
524
+ },
525
+ {
526
+ "name": "India",
527
+ "numeric3": "356",
528
+ "alpha2": "IN"
529
+ },
530
+ {
531
+ "name": "Indonesia",
532
+ "numeric3": "360",
533
+ "alpha2": "ID"
534
+ },
535
+ {
536
+ "name": "Iran (Islamic Republic of)",
537
+ "numeric3": "364",
538
+ "alpha2": "IR"
539
+ },
540
+ {
541
+ "name": "Iraq",
542
+ "numeric3": "368",
543
+ "alpha2": "IQ"
544
+ },
545
+ {
546
+ "name": "Ireland",
547
+ "numeric3": "372",
548
+ "alpha2": "IE"
549
+ },
550
+ {
551
+ "name": "Isle of Man",
552
+ "numeric3": "833",
553
+ "alpha2": "IM"
554
+ },
555
+ {
556
+ "name": "Israel",
557
+ "numeric3": "376",
558
+ "alpha2": "IL"
559
+ },
560
+ {
561
+ "name": "Italy",
562
+ "numeric3": "380",
563
+ "alpha2": "IT"
564
+ },
565
+ {
566
+ "name": "Jamaica",
567
+ "numeric3": "388",
568
+ "alpha2": "JM"
569
+ },
570
+ {
571
+ "name": "Japan",
572
+ "numeric3": "392",
573
+ "alpha2": "JP"
574
+ },
575
+ {
576
+ "name": "Jersey",
577
+ "numeric3": "832",
578
+ "alpha2": "JE"
579
+ },
580
+ {
581
+ "name": "Jordan",
582
+ "numeric3": "400",
583
+ "alpha2": "JO"
584
+ },
585
+ {
586
+ "name": "Kazakhstan",
587
+ "numeric3": "398",
588
+ "alpha2": "KZ"
589
+ },
590
+ {
591
+ "name": "Kenya",
592
+ "numeric3": "404",
593
+ "alpha2": "KE"
594
+ },
595
+ {
596
+ "name": "Kiribati",
597
+ "numeric3": "296",
598
+ "alpha2": "KI"
599
+ },
600
+ {
601
+ "name": "Korea (the Democratic People's Republic of)",
602
+ "numeric3": "408",
603
+ "alpha2": "KP"
604
+ },
605
+ {
606
+ "name": "Korea (the Republic of)",
607
+ "numeric3": "410",
608
+ "alpha2": "KR"
609
+ },
610
+ {
611
+ "name": "Kuwait",
612
+ "numeric3": "414",
613
+ "alpha2": "KW"
614
+ },
615
+ {
616
+ "name": "Kyrgyzstan",
617
+ "numeric3": "417",
618
+ "alpha2": "KG"
619
+ },
620
+ {
621
+ "name": "Lao People's Democratic Republic (the)",
622
+ "numeric3": "418",
623
+ "alpha2": "LA"
624
+ },
625
+ {
626
+ "name": "Latvia",
627
+ "numeric3": "428",
628
+ "alpha2": "LV"
629
+ },
630
+ {
631
+ "name": "Lebanon",
632
+ "numeric3": "422",
633
+ "alpha2": "LB"
634
+ },
635
+ {
636
+ "name": "Lesotho",
637
+ "numeric3": "426",
638
+ "alpha2": "LS"
639
+ },
640
+ {
641
+ "name": "Liberia",
642
+ "numeric3": "430",
643
+ "alpha2": "LR"
644
+ },
645
+ {
646
+ "name": "Libya",
647
+ "numeric3": "434",
648
+ "alpha2": "LY"
649
+ },
650
+ {
651
+ "name": "Liechtenstein",
652
+ "numeric3": "438",
653
+ "alpha2": "LI"
654
+ },
655
+ {
656
+ "name": "Lithuania",
657
+ "numeric3": "440",
658
+ "alpha2": "LT"
659
+ },
660
+ {
661
+ "name": "Luxembourg",
662
+ "numeric3": "442",
663
+ "alpha2": "LU"
664
+ },
665
+ {
666
+ "name": "Macao",
667
+ "numeric3": "446",
668
+ "alpha2": "MO"
669
+ },
670
+ {
671
+ "name": "North Macedonia",
672
+ "numeric3": "807",
673
+ "alpha2": "MK"
674
+ },
675
+ {
676
+ "name": "Madagascar",
677
+ "numeric3": "450",
678
+ "alpha2": "MG"
679
+ },
680
+ {
681
+ "name": "Malawi",
682
+ "numeric3": "454",
683
+ "alpha2": "MW"
684
+ },
685
+ {
686
+ "name": "Malaysia",
687
+ "numeric3": "458",
688
+ "alpha2": "MY"
689
+ },
690
+ {
691
+ "name": "Maldives",
692
+ "numeric3": "462",
693
+ "alpha2": "MV"
694
+ },
695
+ {
696
+ "name": "Mali",
697
+ "numeric3": "466",
698
+ "alpha2": "ML"
699
+ },
700
+ {
701
+ "name": "Malta",
702
+ "numeric3": "470",
703
+ "alpha2": "MT"
704
+ },
705
+ {
706
+ "name": "Marshall Islands (the)",
707
+ "numeric3": "584",
708
+ "alpha2": "MH"
709
+ },
710
+ {
711
+ "name": "Martinique",
712
+ "numeric3": "474",
713
+ "alpha2": "MQ"
714
+ },
715
+ {
716
+ "name": "Mauritania",
717
+ "numeric3": "478",
718
+ "alpha2": "MR"
719
+ },
720
+ {
721
+ "name": "Mauritius",
722
+ "numeric3": "480",
723
+ "alpha2": "MU"
724
+ },
725
+ {
726
+ "name": "Mayotte",
727
+ "numeric3": "175",
728
+ "alpha2": "YT"
729
+ },
730
+ {
731
+ "name": "Mexico",
732
+ "numeric3": "484",
733
+ "alpha2": "MX"
734
+ },
735
+ {
736
+ "name": "Micronesia (Federated States of)",
737
+ "numeric3": "583",
738
+ "alpha2": "FM"
739
+ },
740
+ {
741
+ "name": "Moldova (the Republic of)",
742
+ "numeric3": "498",
743
+ "alpha2": "MD"
744
+ },
745
+ {
746
+ "name": "Monaco",
747
+ "numeric3": "492",
748
+ "alpha2": "MC"
749
+ },
750
+ {
751
+ "name": "Mongolia",
752
+ "numeric3": "496",
753
+ "alpha2": "MN"
754
+ },
755
+ {
756
+ "name": "Montenegro",
757
+ "numeric3": "499",
758
+ "alpha2": "ME"
759
+ },
760
+ {
761
+ "name": "Montserrat",
762
+ "numeric3": "500",
763
+ "alpha2": "MS"
764
+ },
765
+ {
766
+ "name": "Morocco",
767
+ "numeric3": "504",
768
+ "alpha2": "MA"
769
+ },
770
+ {
771
+ "name": "Mozambique",
772
+ "numeric3": "508",
773
+ "alpha2": "MZ"
774
+ },
775
+ {
776
+ "name": "Myanmar",
777
+ "numeric3": "104",
778
+ "alpha2": "MM"
779
+ },
780
+ {
781
+ "name": "Namibia",
782
+ "numeric3": "516",
783
+ "alpha2": "NA"
784
+ },
785
+ {
786
+ "name": "Nauru",
787
+ "numeric3": "520",
788
+ "alpha2": "NR"
789
+ },
790
+ {
791
+ "name": "Nepal",
792
+ "numeric3": "524",
793
+ "alpha2": "NP"
794
+ },
795
+ {
796
+ "name": "Netherlands (the)",
797
+ "numeric3": "528",
798
+ "alpha2": "NL"
799
+ },
800
+ {
801
+ "name": "New Caledonia",
802
+ "numeric3": "540",
803
+ "alpha2": "NC"
804
+ },
805
+ {
806
+ "name": "New Zealand",
807
+ "numeric3": "554",
808
+ "alpha2": "NZ"
809
+ },
810
+ {
811
+ "name": "Nicaragua",
812
+ "numeric3": "558",
813
+ "alpha2": "NI"
814
+ },
815
+ {
816
+ "name": "Niger (the)",
817
+ "numeric3": "562",
818
+ "alpha2": "NE"
819
+ },
820
+ {
821
+ "name": "Nigeria",
822
+ "numeric3": "566",
823
+ "alpha2": "NG"
824
+ },
825
+ {
826
+ "name": "Niue",
827
+ "numeric3": "570",
828
+ "alpha2": "NU"
829
+ },
830
+ {
831
+ "name": "Norfolk Island",
832
+ "numeric3": "574",
833
+ "alpha2": "NF"
834
+ },
835
+ {
836
+ "name": "Northern Mariana Islands (the)",
837
+ "numeric3": "580",
838
+ "alpha2": "MP"
839
+ },
840
+ {
841
+ "name": "Norway",
842
+ "numeric3": "578",
843
+ "alpha2": "NO"
844
+ },
845
+ {
846
+ "name": "Oman",
847
+ "numeric3": "512",
848
+ "alpha2": "OM"
849
+ },
850
+ {
851
+ "name": "Pakistan",
852
+ "numeric3": "586",
853
+ "alpha2": "PK"
854
+ },
855
+ {
856
+ "name": "Palau",
857
+ "numeric3": "585",
858
+ "alpha2": "PW"
859
+ },
860
+ {
861
+ "name": "Palestine, State of",
862
+ "numeric3": "275",
863
+ "alpha2": "PS"
864
+ },
865
+ {
866
+ "name": "Panama",
867
+ "numeric3": "591",
868
+ "alpha2": "PA"
869
+ },
870
+ {
871
+ "name": "Papua New Guinea",
872
+ "numeric3": "598",
873
+ "alpha2": "PG"
874
+ },
875
+ {
876
+ "name": "Paraguay",
877
+ "numeric3": "600",
878
+ "alpha2": "PY"
879
+ },
880
+ {
881
+ "name": "Peru",
882
+ "numeric3": "604",
883
+ "alpha2": "PE"
884
+ },
885
+ {
886
+ "name": "Philippines (the)",
887
+ "numeric3": "608",
888
+ "alpha2": "PH"
889
+ },
890
+ {
891
+ "name": "Pitcairn",
892
+ "numeric3": "612",
893
+ "alpha2": "PN"
894
+ },
895
+ {
896
+ "name": "Poland",
897
+ "numeric3": "616",
898
+ "alpha2": "PL"
899
+ },
900
+ {
901
+ "name": "Portugal",
902
+ "numeric3": "620",
903
+ "alpha2": "PT"
904
+ },
905
+ {
906
+ "name": "Puerto Rico",
907
+ "numeric3": "630",
908
+ "alpha2": "PR"
909
+ },
910
+ {
911
+ "name": "Qatar",
912
+ "numeric3": "634",
913
+ "alpha2": "QA"
914
+ },
915
+ {
916
+ "name": "Réunion",
917
+ "numeric3": "638",
918
+ "alpha2": "RE"
919
+ },
920
+ {
921
+ "name": "Romania",
922
+ "numeric3": "642",
923
+ "alpha2": "RO"
924
+ },
925
+ {
926
+ "name": "Russian Federation (the)",
927
+ "numeric3": "643",
928
+ "alpha2": "RU"
929
+ },
930
+ {
931
+ "name": "Rwanda",
932
+ "numeric3": "646",
933
+ "alpha2": "RW"
934
+ },
935
+ {
936
+ "name": "Saint Barthélemy",
937
+ "numeric3": "652",
938
+ "alpha2": "BL"
939
+ },
940
+ {
941
+ "name": "Saint Helena Ascension Island Tristan da Cunha",
942
+ "numeric3": "654",
943
+ "alpha2": "SH"
944
+ },
945
+ {
946
+ "name": "Saint Kitts and Nevis",
947
+ "numeric3": "659",
948
+ "alpha2": "KN"
949
+ },
950
+ {
951
+ "name": "Saint Lucia",
952
+ "numeric3": "662",
953
+ "alpha2": "LC"
954
+ },
955
+ {
956
+ "name": "Saint Martin (French part)",
957
+ "numeric3": "663",
958
+ "alpha2": "MF"
959
+ },
960
+ {
961
+ "name": "Saint Pierre and Miquelon",
962
+ "numeric3": "666",
963
+ "alpha2": "PM"
964
+ },
965
+ {
966
+ "name": "Saint Vincent and the Grenadines",
967
+ "numeric3": "670",
968
+ "alpha2": "VC"
969
+ },
970
+ {
971
+ "name": "Samoa",
972
+ "numeric3": "882",
973
+ "alpha2": "WS"
974
+ },
975
+ {
976
+ "name": "San Marino",
977
+ "numeric3": "674",
978
+ "alpha2": "SM"
979
+ },
980
+ {
981
+ "name": "Sao Tome and Principe",
982
+ "numeric3": "678",
983
+ "alpha2": "ST"
984
+ },
985
+ {
986
+ "name": "Saudi Arabia",
987
+ "numeric3": "682",
988
+ "alpha2": "SA"
989
+ },
990
+ {
991
+ "name": "Senegal",
992
+ "numeric3": "686",
993
+ "alpha2": "SN"
994
+ },
995
+ {
996
+ "name": "Serbia",
997
+ "numeric3": "688",
998
+ "alpha2": "RS"
999
+ },
1000
+ {
1001
+ "name": "Seychelles",
1002
+ "numeric3": "690",
1003
+ "alpha2": "SC"
1004
+ },
1005
+ {
1006
+ "name": "Sierra Leone",
1007
+ "numeric3": "694",
1008
+ "alpha2": "SL"
1009
+ },
1010
+ {
1011
+ "name": "Singapore",
1012
+ "numeric3": "702",
1013
+ "alpha2": "SG"
1014
+ },
1015
+ {
1016
+ "name": "Sint Maarten (Dutch part)",
1017
+ "numeric3": "534",
1018
+ "alpha2": "SX"
1019
+ },
1020
+ {
1021
+ "name": "Slovakia",
1022
+ "numeric3": "703",
1023
+ "alpha2": "SK"
1024
+ },
1025
+ {
1026
+ "name": "Slovenia",
1027
+ "numeric3": "705",
1028
+ "alpha2": "SI"
1029
+ },
1030
+ {
1031
+ "name": "Solomon Islands",
1032
+ "numeric3": "090",
1033
+ "alpha2": "SB"
1034
+ },
1035
+ {
1036
+ "name": "Somalia",
1037
+ "numeric3": "706",
1038
+ "alpha2": "SO"
1039
+ },
1040
+ {
1041
+ "name": "South Africa",
1042
+ "numeric3": "710",
1043
+ "alpha2": "ZA"
1044
+ },
1045
+ {
1046
+ "name": "South Georgia and the South Sandwich Islands",
1047
+ "numeric3": "239",
1048
+ "alpha2": "GS"
1049
+ },
1050
+ {
1051
+ "name": "South Sudan",
1052
+ "numeric3": "728",
1053
+ "alpha2": "SS"
1054
+ },
1055
+ {
1056
+ "name": "Spain",
1057
+ "numeric3": "724",
1058
+ "alpha2": "ES"
1059
+ },
1060
+ {
1061
+ "name": "Sri Lanka",
1062
+ "numeric3": "144",
1063
+ "alpha2": "LK"
1064
+ },
1065
+ {
1066
+ "name": "Sudan (the)",
1067
+ "numeric3": "729",
1068
+ "alpha2": "SD"
1069
+ },
1070
+ {
1071
+ "name": "Suriname",
1072
+ "numeric3": "740",
1073
+ "alpha2": "SR"
1074
+ },
1075
+ {
1076
+ "name": "Svalbard Jan Mayen",
1077
+ "numeric3": "744",
1078
+ "alpha2": "SJ"
1079
+ },
1080
+ {
1081
+ "name": "Sweden",
1082
+ "numeric3": "752",
1083
+ "alpha2": "SE"
1084
+ },
1085
+ {
1086
+ "name": "Switzerland",
1087
+ "numeric3": "756",
1088
+ "alpha2": "CH"
1089
+ },
1090
+ {
1091
+ "name": "Syrian Arab Republic (the)",
1092
+ "numeric3": "760",
1093
+ "alpha2": "SY"
1094
+ },
1095
+ {
1096
+ "name": "Taiwan (Province of China)",
1097
+ "numeric3": "158",
1098
+ "alpha2": "TW"
1099
+ },
1100
+ {
1101
+ "name": "Tajikistan",
1102
+ "numeric3": "762",
1103
+ "alpha2": "TJ"
1104
+ },
1105
+ {
1106
+ "name": "Tanzania, the United Republic of",
1107
+ "numeric3": "834",
1108
+ "alpha2": "TZ"
1109
+ },
1110
+ {
1111
+ "name": "Thailand",
1112
+ "numeric3": "764",
1113
+ "alpha2": "TH"
1114
+ },
1115
+ {
1116
+ "name": "Timor-Leste",
1117
+ "numeric3": "626",
1118
+ "alpha2": "TL"
1119
+ },
1120
+ {
1121
+ "name": "Togo",
1122
+ "numeric3": "768",
1123
+ "alpha2": "TG"
1124
+ },
1125
+ {
1126
+ "name": "Tokelau",
1127
+ "numeric3": "772",
1128
+ "alpha2": "TK"
1129
+ },
1130
+ {
1131
+ "name": "Tonga",
1132
+ "numeric3": "776",
1133
+ "alpha2": "TO"
1134
+ },
1135
+ {
1136
+ "name": "Trinidad and Tobago",
1137
+ "numeric3": "780",
1138
+ "alpha2": "TT"
1139
+ },
1140
+ {
1141
+ "name": "Tunisia",
1142
+ "numeric3": "788",
1143
+ "alpha2": "TN"
1144
+ },
1145
+ {
1146
+ "name": "Turkey",
1147
+ "numeric3": "792",
1148
+ "alpha2": "TR"
1149
+ },
1150
+ {
1151
+ "name": "Turkmenistan",
1152
+ "numeric3": "795",
1153
+ "alpha2": "TM"
1154
+ },
1155
+ {
1156
+ "name": "Turks and Caicos Islands (the)",
1157
+ "numeric3": "796",
1158
+ "alpha2": "TC"
1159
+ },
1160
+ {
1161
+ "name": "Tuvalu",
1162
+ "numeric3": "798",
1163
+ "alpha2": "TV"
1164
+ },
1165
+ {
1166
+ "name": "Uganda",
1167
+ "numeric3": "800",
1168
+ "alpha2": "UG"
1169
+ },
1170
+ {
1171
+ "name": "Ukraine",
1172
+ "numeric3": "804",
1173
+ "alpha2": "UA"
1174
+ },
1175
+ {
1176
+ "name": "United Arab Emirates (the)",
1177
+ "numeric3": "784",
1178
+ "alpha2": "AE"
1179
+ },
1180
+ {
1181
+ "name": "United Kingdom of Great Britain and Northern Ireland (the)",
1182
+ "numeric3": "826",
1183
+ "alpha2": "GB"
1184
+ },
1185
+ {
1186
+ "name": "United States Minor Outlying Islands (the)",
1187
+ "numeric3": "581",
1188
+ "alpha2": "UM"
1189
+ },
1190
+ {
1191
+ "name": "United States of America (the)",
1192
+ "numeric3": "840",
1193
+ "alpha2": "US"
1194
+ },
1195
+ {
1196
+ "name": "Uruguay",
1197
+ "numeric3": "858",
1198
+ "alpha2": "UY"
1199
+ },
1200
+ {
1201
+ "name": "Uzbekistan",
1202
+ "numeric3": "860",
1203
+ "alpha2": "UZ"
1204
+ },
1205
+ {
1206
+ "name": "Vanuatu",
1207
+ "numeric3": "548",
1208
+ "alpha2": "VU"
1209
+ },
1210
+ {
1211
+ "name": "Venezuela (Bolivarian Republic of)",
1212
+ "numeric3": "862",
1213
+ "alpha2": "VE"
1214
+ },
1215
+ {
1216
+ "name": "Viet Nam",
1217
+ "numeric3": "704",
1218
+ "alpha2": "VN"
1219
+ },
1220
+ {
1221
+ "name": "Virgin Islands (British)",
1222
+ "numeric3": "092",
1223
+ "alpha2": "VG"
1224
+ },
1225
+ {
1226
+ "name": "Virgin Islands (U.S.)",
1227
+ "numeric3": "850",
1228
+ "alpha2": "VI"
1229
+ },
1230
+ {
1231
+ "name": "Wallis and Futuna",
1232
+ "numeric3": "876",
1233
+ "alpha2": "WF"
1234
+ },
1235
+ {
1236
+ "name": "Western Sahara",
1237
+ "numeric3": "732",
1238
+ "alpha2": "EH"
1239
+ },
1240
+ {
1241
+ "name": "Yemen",
1242
+ "numeric3": "887",
1243
+ "alpha2": "YE"
1244
+ },
1245
+ {
1246
+ "name": "Zambia",
1247
+ "numeric3": "894",
1248
+ "alpha2": "ZM"
1249
+ },
1250
+ {
1251
+ "name": "Zimbabwe",
1252
+ "numeric3": "716",
1253
+ "alpha2": "ZW"
1254
+ }
1255
+ ];
1256
+
1257
+ return {
1258
+ /**
1259
+ * @param code country code corresponding to alpha2, alpha3 or numeric3
1260
+ * @returns {alpha2: String, numeric3: String, name: String}
1261
+ */
1262
+ lookup: function (code) {
1263
+ if (!code) {
1264
+ return;
1265
+ }
1266
+ for (let i = 0; i < codes.length; i++) {
1267
+ const codeMap = codes[i];
1268
+ if (String(code).toUpperCase() === codeMap.alpha2 || Number(code) === Number(codeMap.numeric3)) {
1269
+ return codeMap;
1270
+ }
1271
+ }
1272
+ },
1273
+ codes: codes
1274
+ }
1275
+ }());
js/iso-639-translator.js ADDED
@@ -0,0 +1,2362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Implementation to quickly translate ISO 639 language codes.
3
+ *
4
+ * Combination of sources
5
+ * @link https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
6
+ * @link https://www.loc.gov/standards/iso639-2/php/code_list.php
7
+ */
8
+ const iso639 = (function () {
9
+ 'use strict';
10
+
11
+ const codes = [
12
+ {
13
+ "639_1": "aa",
14
+ "639_2_T": "aar",
15
+ "639_2_B": "aar",
16
+ "name": "Afar"
17
+ },
18
+ {
19
+ "639_1": "ab",
20
+ "639_2_T": "abk",
21
+ "639_2_B": "abk",
22
+ "name": "Abkhazian"
23
+ },
24
+ {
25
+ "639_2_T": "ace",
26
+ "name": "Achinese"
27
+ },
28
+ {
29
+ "639_2_T": "ach",
30
+ "name": "Acoli"
31
+ },
32
+ {
33
+ "639_2_T": "ada",
34
+ "name": "Adangme"
35
+ },
36
+ {
37
+ "639_2_T": "ady",
38
+ "name": "Adyghe; Adygei"
39
+ },
40
+ {
41
+ "639_2_T": "afa",
42
+ "name": "Afro-Asiatic languages"
43
+ },
44
+ {
45
+ "639_2_T": "afh",
46
+ "name": "Afrihili"
47
+ },
48
+ {
49
+ "639_1": "af",
50
+ "639_2_T": "afr",
51
+ "639_2_B": "afr",
52
+ "name": "Afrikaans"
53
+ },
54
+ {
55
+ "639_2_T": "ain",
56
+ "name": "Ainu"
57
+ },
58
+ {
59
+ "639_1": "ak",
60
+ "639_2_T": "aka",
61
+ "639_2_B": "aka",
62
+ "name": "Akan"
63
+ },
64
+ {
65
+ "639_2_T": "akk",
66
+ "name": "Akkadian"
67
+ },
68
+ {
69
+ "639_2_T": "ale",
70
+ "name": "Aleut"
71
+ },
72
+ {
73
+ "639_2_T": "alg",
74
+ "name": "Algonquian languages"
75
+ },
76
+ {
77
+ "639_2_T": "alt",
78
+ "name": "Southern Altai"
79
+ },
80
+ {
81
+ "639_1": "am",
82
+ "639_2_T": "amh",
83
+ "639_2_B": "amh",
84
+ "name": "Amharic"
85
+ },
86
+ {
87
+ "639_2_T": "ang",
88
+ "name": "English, Old (ca.450-1100)"
89
+ },
90
+ {
91
+ "639_2_T": "anp",
92
+ "name": "Angika"
93
+ },
94
+ {
95
+ "639_2_T": "apa",
96
+ "name": "Apache languages"
97
+ },
98
+ {
99
+ "639_1": "ar",
100
+ "639_2_T": "ara",
101
+ "639_2_B": "ara",
102
+ "name": "Arabic"
103
+ },
104
+ {
105
+ "639_2_T": "arc",
106
+ "name": "Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)"
107
+ },
108
+ {
109
+ "639_1": "an",
110
+ "639_2_T": "arg",
111
+ "639_2_B": "arg",
112
+ "name": "Aragonese"
113
+ },
114
+ {
115
+ "639_2_T": "arn",
116
+ "name": "Mapudungun; Mapuche"
117
+ },
118
+ {
119
+ "639_2_T": "arp",
120
+ "name": "Arapaho"
121
+ },
122
+ {
123
+ "639_2_T": "art",
124
+ "name": "Artificial languages"
125
+ },
126
+ {
127
+ "639_2_T": "arw",
128
+ "name": "Arawak"
129
+ },
130
+ {
131
+ "639_1": "as",
132
+ "639_2_T": "asm",
133
+ "639_2_B": "asm",
134
+ "name": "Assamese"
135
+ },
136
+ {
137
+ "639_2_T": "ast",
138
+ "name": "Asturian; Bable; Leonese; Asturleonese"
139
+ },
140
+ {
141
+ "639_2_T": "ath",
142
+ "name": "Athapascan languages"
143
+ },
144
+ {
145
+ "639_2_T": "aus",
146
+ "name": "Australian languages"
147
+ },
148
+ {
149
+ "639_1": "av",
150
+ "639_2_T": "ava",
151
+ "639_2_B": "ava",
152
+ "name": "Avaric"
153
+ },
154
+ {
155
+ "639_1": "ae",
156
+ "639_2_T": "ave",
157
+ "639_2_B": "ave",
158
+ "name": "Avestan"
159
+ },
160
+ {
161
+ "639_2_T": "awa",
162
+ "name": "Awadhi"
163
+ },
164
+ {
165
+ "639_1": "ay",
166
+ "639_2_T": "aym",
167
+ "639_2_B": "aym",
168
+ "name": "Aymara"
169
+ },
170
+ {
171
+ "639_1": "az",
172
+ "639_2_T": "aze",
173
+ "639_2_B": "aze",
174
+ "name": "Azerbaijani"
175
+ },
176
+ {
177
+ "639_2_T": "bad",
178
+ "name": "Banda languages"
179
+ },
180
+ {
181
+ "639_2_T": "bai",
182
+ "name": "Bamileke languages"
183
+ },
184
+ {
185
+ "639_1": "ba",
186
+ "639_2_T": "bak",
187
+ "639_2_B": "bak",
188
+ "name": "Bashkir"
189
+ },
190
+ {
191
+ "639_2_T": "bal",
192
+ "name": "Baluchi"
193
+ },
194
+ {
195
+ "639_1": "bm",
196
+ "639_2_T": "bam",
197
+ "639_2_B": "bam",
198
+ "name": "Bambara"
199
+ },
200
+ {
201
+ "639_2_T": "ban",
202
+ "name": "Balinese"
203
+ },
204
+ {
205
+ "639_2_T": "bas",
206
+ "name": "Basa"
207
+ },
208
+ {
209
+ "639_2_T": "bat",
210
+ "name": "Baltic languages"
211
+ },
212
+ {
213
+ "639_2_T": "bej",
214
+ "name": "Beja; Bedawiyet"
215
+ },
216
+ {
217
+ "639_1": "be",
218
+ "639_2_T": "bel",
219
+ "639_2_B": "bel",
220
+ "name": "Belarusian"
221
+ },
222
+ {
223
+ "639_2_T": "bem",
224
+ "name": "Bemba"
225
+ },
226
+ {
227
+ "639_1": "bn",
228
+ "639_2_T": "ben",
229
+ "639_2_B": "ben",
230
+ "name": "Bengali"
231
+ },
232
+ {
233
+ "639_2_T": "ber",
234
+ "name": "Berber languages"
235
+ },
236
+ {
237
+ "639_2_T": "bho",
238
+ "name": "Bhojpuri"
239
+ },
240
+ {
241
+ "639_1": "bh",
242
+ "639_2_T": "bih",
243
+ "639_2_B": "bih",
244
+ "name": "Bihari languages"
245
+ },
246
+ {
247
+ "639_2_T": "bik",
248
+ "name": "Bikol"
249
+ },
250
+ {
251
+ "639_2_T": "bin",
252
+ "name": "Bini; Edo"
253
+ },
254
+ {
255
+ "639_1": "bi",
256
+ "639_2_T": "bis",
257
+ "639_2_B": "bis",
258
+ "name": "Bislama"
259
+ },
260
+ {
261
+ "639_2_T": "bla",
262
+ "name": "Siksika"
263
+ },
264
+ {
265
+ "639_2_T": "bnt",
266
+ "name": "Bantu languages"
267
+ },
268
+ {
269
+ "639_1": "bo",
270
+ "639_2_T": "bod",
271
+ "639_2_B": "tib",
272
+ "name": "Tibetan"
273
+ },
274
+ {
275
+ "639_1": "bs",
276
+ "639_2_T": "bos",
277
+ "639_2_B": "bos",
278
+ "name": "Bosnian"
279
+ },
280
+ {
281
+ "639_2_T": "bra",
282
+ "name": "Braj"
283
+ },
284
+ {
285
+ "639_1": "br",
286
+ "639_2_T": "bre",
287
+ "639_2_B": "bre",
288
+ "name": "Breton"
289
+ },
290
+ {
291
+ "639_2_T": "btk",
292
+ "name": "Batak languages"
293
+ },
294
+ {
295
+ "639_2_T": "bua",
296
+ "name": "Buriat"
297
+ },
298
+ {
299
+ "639_2_T": "bug",
300
+ "name": "Buginese"
301
+ },
302
+ {
303
+ "639_1": "bg",
304
+ "639_2_T": "bul",
305
+ "639_2_B": "bul",
306
+ "name": "Bulgarian"
307
+ },
308
+ {
309
+ "639_2_T": "byn",
310
+ "name": "Blin; Bilin"
311
+ },
312
+ {
313
+ "639_2_T": "cad",
314
+ "name": "Caddo"
315
+ },
316
+ {
317
+ "639_2_T": "cai",
318
+ "name": "Central American Indian languages"
319
+ },
320
+ {
321
+ "639_2_T": "car",
322
+ "name": "Galibi Carib"
323
+ },
324
+ {
325
+ "639_1": "ca",
326
+ "639_2_T": "cat",
327
+ "639_2_B": "cat",
328
+ "name": "Catalan, Valencian"
329
+ },
330
+ {
331
+ "639_2_T": "cau",
332
+ "name": "Caucasian languages"
333
+ },
334
+ {
335
+ "639_2_T": "ceb",
336
+ "name": "Cebuano"
337
+ },
338
+ {
339
+ "639_2_T": "cel",
340
+ "name": "Celtic languages"
341
+ },
342
+ {
343
+ "639_1": "cs",
344
+ "639_2_T": "ces",
345
+ "639_2_B": "cze",
346
+ "name": "Czech"
347
+ },
348
+ {
349
+ "639_1": "ch",
350
+ "639_2_T": "cha",
351
+ "639_2_B": "cha",
352
+ "name": "Chamorro"
353
+ },
354
+ {
355
+ "639_2_T": "chb",
356
+ "name": "Chibcha"
357
+ },
358
+ {
359
+ "639_1": "ce",
360
+ "639_2_T": "che",
361
+ "639_2_B": "che",
362
+ "name": "Chechen"
363
+ },
364
+ {
365
+ "639_2_T": "chg",
366
+ "name": "Chagatai"
367
+ },
368
+ {
369
+ "639_2_T": "chk",
370
+ "name": "Chuukese"
371
+ },
372
+ {
373
+ "639_2_T": "chm",
374
+ "name": "Mari"
375
+ },
376
+ {
377
+ "639_2_T": "chn",
378
+ "name": "Chinook jargon"
379
+ },
380
+ {
381
+ "639_2_T": "cho",
382
+ "name": "Choctaw"
383
+ },
384
+ {
385
+ "639_2_T": "chp",
386
+ "name": "Chipewyan; Dene Suline"
387
+ },
388
+ {
389
+ "639_2_T": "chr",
390
+ "name": "Cherokee"
391
+ },
392
+ {
393
+ "639_1": "cu",
394
+ "639_2_T": "chu",
395
+ "639_2_B": "chu",
396
+ "name": "Church Slavic, Old Slavonic, Church Slavonic, Old Bulgarian, Old Church Slavonic"
397
+ },
398
+ {
399
+ "639_1": "cv",
400
+ "639_2_T": "chv",
401
+ "639_2_B": "chv",
402
+ "name": "Chuvash"
403
+ },
404
+ {
405
+ "639_2_T": "chy",
406
+ "name": "Cheyenne"
407
+ },
408
+ {
409
+ "639_2_T": "cmc",
410
+ "name": "Chamic languages"
411
+ },
412
+ {
413
+ "639_2_T": "cnr",
414
+ "name": "Montenegrin"
415
+ },
416
+ {
417
+ "639_2_T": "cop",
418
+ "name": "Coptic"
419
+ },
420
+ {
421
+ "639_1": "kw",
422
+ "639_2_T": "cor",
423
+ "639_2_B": "cor",
424
+ "name": "Cornish"
425
+ },
426
+ {
427
+ "639_1": "co",
428
+ "639_2_T": "cos",
429
+ "639_2_B": "cos",
430
+ "name": "Corsican"
431
+ },
432
+ {
433
+ "639_2_T": "cpe",
434
+ "name": "Creoles and pidgins, English based"
435
+ },
436
+ {
437
+ "639_2_T": "cpf",
438
+ "name": "Creoles and pidgins, French-based"
439
+ },
440
+ {
441
+ "639_2_T": "cpp",
442
+ "name": "Creoles and pidgins, Portuguese-based"
443
+ },
444
+ {
445
+ "639_1": "cr",
446
+ "639_2_T": "cre",
447
+ "639_2_B": "cre",
448
+ "name": "Cree"
449
+ },
450
+ {
451
+ "639_2_T": "crh",
452
+ "name": "Crimean Tatar; Crimean Turkish"
453
+ },
454
+ {
455
+ "639_2_T": "crp",
456
+ "name": "Creoles and pidgins"
457
+ },
458
+ {
459
+ "639_2_T": "csb",
460
+ "name": "Kashubian"
461
+ },
462
+ {
463
+ "639_2_T": "cus",
464
+ "name": "Cushitic languages"
465
+ },
466
+ {
467
+ "639_1": "cy",
468
+ "639_2_T": "cym",
469
+ "639_2_B": "wel",
470
+ "name": "Welsh"
471
+ },
472
+ {
473
+ "639_2_T": "dak",
474
+ "name": "Dakota"
475
+ },
476
+ {
477
+ "639_1": "da",
478
+ "639_2_T": "dan",
479
+ "639_2_B": "dan",
480
+ "name": "Danish"
481
+ },
482
+ {
483
+ "639_2_T": "dar",
484
+ "name": "Dargwa"
485
+ },
486
+ {
487
+ "639_2_T": "day",
488
+ "name": "Land Dayak languages"
489
+ },
490
+ {
491
+ "639_2_T": "del",
492
+ "name": "Delaware"
493
+ },
494
+ {
495
+ "639_2_T": "den",
496
+ "name": "Slave (Athapascan)"
497
+ },
498
+ {
499
+ "639_1": "de",
500
+ "639_2_T": "deu",
501
+ "639_2_B": "ger",
502
+ "name": "German"
503
+ },
504
+ {
505
+ "639_2_T": "dgr",
506
+ "name": "Dogrib"
507
+ },
508
+ {
509
+ "639_2_T": "din",
510
+ "name": "Dinka"
511
+ },
512
+ {
513
+ "639_1": "dv",
514
+ "639_2_T": "div",
515
+ "639_2_B": "div",
516
+ "name": "Divehi, Dhivehi, Maldivian"
517
+ },
518
+ {
519
+ "639_2_T": "doi",
520
+ "name": "Dogri"
521
+ },
522
+ {
523
+ "639_2_T": "dra",
524
+ "name": "Dravidian languages"
525
+ },
526
+ {
527
+ "639_2_T": "dsb",
528
+ "name": "Lower Sorbian"
529
+ },
530
+ {
531
+ "639_2_T": "dua",
532
+ "name": "Duala"
533
+ },
534
+ {
535
+ "639_2_T": "dum",
536
+ "name": "Dutch, Middle (ca.1050-1350)"
537
+ },
538
+ {
539
+ "639_2_T": "dyu",
540
+ "name": "Dyula"
541
+ },
542
+ {
543
+ "639_1": "dz",
544
+ "639_2_T": "dzo",
545
+ "639_2_B": "dzo",
546
+ "name": "Dzongkha"
547
+ },
548
+ {
549
+ "639_2_T": "efi",
550
+ "name": "Efik"
551
+ },
552
+ {
553
+ "639_2_T": "egy",
554
+ "name": "Egyptian (Ancient)"
555
+ },
556
+ {
557
+ "639_2_T": "eka",
558
+ "name": "Ekajuk"
559
+ },
560
+ {
561
+ "639_1": "el",
562
+ "639_2_T": "ell",
563
+ "639_2_B": "gre",
564
+ "name": "Greek, Modern (1453-)"
565
+ },
566
+ {
567
+ "639_2_T": "elx",
568
+ "name": "Elamite"
569
+ },
570
+ {
571
+ "639_1": "en",
572
+ "639_2_T": "eng",
573
+ "639_2_B": "eng",
574
+ "name": "English"
575
+ },
576
+ {
577
+ "639_2_T": "enm",
578
+ "name": "English, Middle (1100-1500)"
579
+ },
580
+ {
581
+ "639_1": "eo",
582
+ "639_2_T": "epo",
583
+ "639_2_B": "epo",
584
+ "name": "Esperanto"
585
+ },
586
+ {
587
+ "639_1": "et",
588
+ "639_2_T": "est",
589
+ "639_2_B": "est",
590
+ "name": "Estonian"
591
+ },
592
+ {
593
+ "639_1": "eu",
594
+ "639_2_T": "eus",
595
+ "639_2_B": "baq",
596
+ "name": "Basque"
597
+ },
598
+ {
599
+ "639_1": "ee",
600
+ "639_2_T": "ewe",
601
+ "639_2_B": "ewe",
602
+ "name": "Ewe"
603
+ },
604
+ {
605
+ "639_2_T": "ewo",
606
+ "name": "Ewondo"
607
+ },
608
+ {
609
+ "639_2_T": "fan",
610
+ "name": "Fang"
611
+ },
612
+ {
613
+ "639_1": "fo",
614
+ "639_2_T": "fao",
615
+ "639_2_B": "fao",
616
+ "name": "Faroese"
617
+ },
618
+ {
619
+ "639_1": "fa",
620
+ "639_2_T": "fas",
621
+ "639_2_B": "per",
622
+ "name": "Persian"
623
+ },
624
+ {
625
+ "639_2_T": "fat",
626
+ "name": "Fanti"
627
+ },
628
+ {
629
+ "639_1": "fj",
630
+ "639_2_T": "fij",
631
+ "639_2_B": "fij",
632
+ "name": "Fijian"
633
+ },
634
+ {
635
+ "639_2_T": "fil",
636
+ "name": "Filipino; Pilipino"
637
+ },
638
+ {
639
+ "639_1": "fi",
640
+ "639_2_T": "fin",
641
+ "639_2_B": "fin",
642
+ "name": "Finnish"
643
+ },
644
+ {
645
+ "639_2_T": "fiu",
646
+ "name": "Finno-Ugrian languages"
647
+ },
648
+ {
649
+ "639_2_T": "fon",
650
+ "name": "Fon"
651
+ },
652
+ {
653
+ "639_1": "fr",
654
+ "639_2_T": "fra",
655
+ "639_2_B": "fre",
656
+ "name": "French"
657
+ },
658
+ {
659
+ "639_2_T": "frm",
660
+ "name": "French, Middle (ca.1400-1600)"
661
+ },
662
+ {
663
+ "639_2_T": "fro",
664
+ "name": "French, Old (842-ca.1400)"
665
+ },
666
+ {
667
+ "639_2_T": "frr",
668
+ "name": "Northern Frisian"
669
+ },
670
+ {
671
+ "639_2_T": "frs",
672
+ "name": "Eastern Frisian"
673
+ },
674
+ {
675
+ "639_1": "fy",
676
+ "639_2_T": "fry",
677
+ "639_2_B": "fry",
678
+ "name": "Western Frisian"
679
+ },
680
+ {
681
+ "639_1": "ff",
682
+ "639_2_T": "ful",
683
+ "639_2_B": "ful",
684
+ "name": "Fulah"
685
+ },
686
+ {
687
+ "639_2_T": "fur",
688
+ "name": "Friulian"
689
+ },
690
+ {
691
+ "639_2_T": "gaa",
692
+ "name": "Ga"
693
+ },
694
+ {
695
+ "639_2_T": "gay",
696
+ "name": "Gayo"
697
+ },
698
+ {
699
+ "639_2_T": "gba",
700
+ "name": "Gbaya"
701
+ },
702
+ {
703
+ "639_2_T": "gem",
704
+ "name": "Germanic languages"
705
+ },
706
+ {
707
+ "639_2_T": "gez",
708
+ "name": "Geez"
709
+ },
710
+ {
711
+ "639_2_T": "gil",
712
+ "name": "Gilbertese"
713
+ },
714
+ {
715
+ "639_1": "gd",
716
+ "639_2_T": "gla",
717
+ "639_2_B": "gla",
718
+ "name": "Gaelic, Scottish Gaelic"
719
+ },
720
+ {
721
+ "639_1": "ga",
722
+ "639_2_T": "gle",
723
+ "639_2_B": "gle",
724
+ "name": "Irish"
725
+ },
726
+ {
727
+ "639_1": "gl",
728
+ "639_2_T": "glg",
729
+ "639_2_B": "glg",
730
+ "name": "Galician"
731
+ },
732
+ {
733
+ "639_1": "gv",
734
+ "639_2_T": "glv",
735
+ "639_2_B": "glv",
736
+ "name": "Manx"
737
+ },
738
+ {
739
+ "639_2_T": "gmh",
740
+ "name": "German, Middle High (ca.1050-1500)"
741
+ },
742
+ {
743
+ "639_2_T": "goh",
744
+ "name": "German, Old High (ca.750-1050)"
745
+ },
746
+ {
747
+ "639_2_T": "gon",
748
+ "name": "Gondi"
749
+ },
750
+ {
751
+ "639_2_T": "gor",
752
+ "name": "Gorontalo"
753
+ },
754
+ {
755
+ "639_2_T": "got",
756
+ "name": "Gothic"
757
+ },
758
+ {
759
+ "639_2_T": "grb",
760
+ "name": "Grebo"
761
+ },
762
+ {
763
+ "639_2_T": "grc",
764
+ "name": "Greek, Ancient (to 1453)"
765
+ },
766
+ {
767
+ "639_1": "gn",
768
+ "639_2_T": "grn",
769
+ "639_2_B": "grn",
770
+ "name": "Guarani"
771
+ },
772
+ {
773
+ "639_2_T": "gsw",
774
+ "name": "Swiss German; Alemannic; Alsatian"
775
+ },
776
+ {
777
+ "639_1": "gu",
778
+ "639_2_T": "guj",
779
+ "639_2_B": "guj",
780
+ "name": "Gujarati"
781
+ },
782
+ {
783
+ "639_2_T": "gwi",
784
+ "name": "Gwich'in"
785
+ },
786
+ {
787
+ "639_2_T": "hai",
788
+ "name": "Haida"
789
+ },
790
+ {
791
+ "639_1": "ht",
792
+ "639_2_T": "hat",
793
+ "639_2_B": "hat",
794
+ "name": "Haitian, Haitian Creole"
795
+ },
796
+ {
797
+ "639_1": "ha",
798
+ "639_2_T": "hau",
799
+ "639_2_B": "hau",
800
+ "name": "Hausa"
801
+ },
802
+ {
803
+ "639_2_T": "haw",
804
+ "name": "Hawaiian"
805
+ },
806
+ {
807
+ "639_1": "he",
808
+ "639_2_T": "heb",
809
+ "639_2_B": "heb",
810
+ "name": "Hebrew"
811
+ },
812
+ {
813
+ "639_1": "hz",
814
+ "639_2_T": "her",
815
+ "639_2_B": "her",
816
+ "name": "Herero"
817
+ },
818
+ {
819
+ "639_2_T": "hil",
820
+ "name": "Hiligaynon"
821
+ },
822
+ {
823
+ "639_2_T": "him",
824
+ "name": "Himachali languages; Western Pahari languages"
825
+ },
826
+ {
827
+ "639_1": "hi",
828
+ "639_2_T": "hin",
829
+ "639_2_B": "hin",
830
+ "name": "Hindi"
831
+ },
832
+ {
833
+ "639_2_T": "hit",
834
+ "name": "Hittite"
835
+ },
836
+ {
837
+ "639_2_T": "hmn",
838
+ "name": "Hmong; Mong"
839
+ },
840
+ {
841
+ "639_1": "ho",
842
+ "639_2_T": "hmo",
843
+ "639_2_B": "hmo",
844
+ "name": "Hiri Motu"
845
+ },
846
+ {
847
+ "639_1": "hr",
848
+ "639_2_T": "hrv",
849
+ "639_2_B": "hrv",
850
+ "name": "Croatian"
851
+ },
852
+ {
853
+ "639_2_T": "hsb",
854
+ "name": "Upper Sorbian"
855
+ },
856
+ {
857
+ "639_1": "hu",
858
+ "639_2_T": "hun",
859
+ "639_2_B": "hun",
860
+ "name": "Hungarian"
861
+ },
862
+ {
863
+ "639_2_T": "hup",
864
+ "name": "Hupa"
865
+ },
866
+ {
867
+ "639_1": "hy",
868
+ "639_2_T": "hye",
869
+ "639_2_B": "arm",
870
+ "name": "Armenian"
871
+ },
872
+ {
873
+ "639_2_T": "iba",
874
+ "name": "Iban"
875
+ },
876
+ {
877
+ "639_1": "ig",
878
+ "639_2_T": "ibo",
879
+ "639_2_B": "ibo",
880
+ "name": "Igbo"
881
+ },
882
+ {
883
+ "639_1": "io",
884
+ "639_2_T": "ido",
885
+ "639_2_B": "ido",
886
+ "name": "Ido"
887
+ },
888
+ {
889
+ "639_1": "ii",
890
+ "639_2_T": "iii",
891
+ "639_2_B": "iii",
892
+ "name": "Sichuan Yi, Nuosu"
893
+ },
894
+ {
895
+ "639_2_T": "ijo",
896
+ "name": "Ijo languages"
897
+ },
898
+ {
899
+ "639_1": "iu",
900
+ "639_2_T": "iku",
901
+ "639_2_B": "iku",
902
+ "name": "Inuktitut"
903
+ },
904
+ {
905
+ "639_1": "ie",
906
+ "639_2_T": "ile",
907
+ "639_2_B": "ile",
908
+ "name": "Interlingue, Occidental"
909
+ },
910
+ {
911
+ "639_2_T": "ilo",
912
+ "name": "Iloko"
913
+ },
914
+ {
915
+ "639_1": "ia",
916
+ "639_2_T": "ina",
917
+ "639_2_B": "ina",
918
+ "name": "Interlingua (International Auxiliary Language Association)"
919
+ },
920
+ {
921
+ "639_2_T": "inc",
922
+ "name": "Indic languages"
923
+ },
924
+ {
925
+ "639_1": "id",
926
+ "639_2_T": "ind",
927
+ "639_2_B": "ind",
928
+ "name": "Indonesian"
929
+ },
930
+ {
931
+ "639_2_T": "ine",
932
+ "name": "Indo-European languages"
933
+ },
934
+ {
935
+ "639_2_T": "inh",
936
+ "name": "Ingush"
937
+ },
938
+ {
939
+ "639_1": "ik",
940
+ "639_2_T": "ipk",
941
+ "639_2_B": "ipk",
942
+ "name": "Inupiaq"
943
+ },
944
+ {
945
+ "639_2_T": "ira",
946
+ "name": "Iranian languages"
947
+ },
948
+ {
949
+ "639_2_T": "iro",
950
+ "name": "Iroquoian languages"
951
+ },
952
+ {
953
+ "639_1": "is",
954
+ "639_2_T": "isl",
955
+ "639_2_B": "ice",
956
+ "name": "Icelandic"
957
+ },
958
+ {
959
+ "639_1": "it",
960
+ "639_2_T": "ita",
961
+ "639_2_B": "ita",
962
+ "name": "Italian"
963
+ },
964
+ {
965
+ "639_1": "jv",
966
+ "639_2_T": "jav",
967
+ "639_2_B": "jav",
968
+ "name": "Javanese"
969
+ },
970
+ {
971
+ "639_2_T": "jbo",
972
+ "name": "Lojban"
973
+ },
974
+ {
975
+ "639_1": "ja",
976
+ "639_2_T": "jpn",
977
+ "639_2_B": "jpn",
978
+ "name": "Japanese"
979
+ },
980
+ {
981
+ "639_2_T": "jpr",
982
+ "name": "Judeo-Persian"
983
+ },
984
+ {
985
+ "639_2_T": "jrb",
986
+ "name": "Judeo-Arabic"
987
+ },
988
+ {
989
+ "639_2_T": "kaa",
990
+ "name": "Kara-Kalpak"
991
+ },
992
+ {
993
+ "639_2_T": "kab",
994
+ "name": "Kabyle"
995
+ },
996
+ {
997
+ "639_2_T": "kac",
998
+ "name": "Kachin; Jingpho"
999
+ },
1000
+ {
1001
+ "639_1": "kl",
1002
+ "639_2_T": "kal",
1003
+ "639_2_B": "kal",
1004
+ "name": "Kalaallisut, Greenlandic"
1005
+ },
1006
+ {
1007
+ "639_2_T": "kam",
1008
+ "name": "Kamba"
1009
+ },
1010
+ {
1011
+ "639_1": "kn",
1012
+ "639_2_T": "kan",
1013
+ "639_2_B": "kan",
1014
+ "name": "Kannada"
1015
+ },
1016
+ {
1017
+ "639_2_T": "kar",
1018
+ "name": "Karen languages"
1019
+ },
1020
+ {
1021
+ "639_1": "ks",
1022
+ "639_2_T": "kas",
1023
+ "639_2_B": "kas",
1024
+ "name": "Kashmiri"
1025
+ },
1026
+ {
1027
+ "639_1": "ka",
1028
+ "639_2_T": "kat",
1029
+ "639_2_B": "geo",
1030
+ "name": "Georgian"
1031
+ },
1032
+ {
1033
+ "639_1": "kr",
1034
+ "639_2_T": "kau",
1035
+ "639_2_B": "kau",
1036
+ "name": "Kanuri"
1037
+ },
1038
+ {
1039
+ "639_2_T": "kaw",
1040
+ "name": "Kawi"
1041
+ },
1042
+ {
1043
+ "639_1": "kk",
1044
+ "639_2_T": "kaz",
1045
+ "639_2_B": "kaz",
1046
+ "name": "Kazakh"
1047
+ },
1048
+ {
1049
+ "639_2_T": "kbd",
1050
+ "name": "Kabardian"
1051
+ },
1052
+ {
1053
+ "639_2_T": "kha",
1054
+ "name": "Khasi"
1055
+ },
1056
+ {
1057
+ "639_2_T": "khi",
1058
+ "name": "Khoisan languages"
1059
+ },
1060
+ {
1061
+ "639_1": "km",
1062
+ "639_2_T": "khm",
1063
+ "639_2_B": "khm",
1064
+ "name": "Central Khmer"
1065
+ },
1066
+ {
1067
+ "639_2_T": "kho",
1068
+ "name": "Khotanese; Sakan"
1069
+ },
1070
+ {
1071
+ "639_1": "ki",
1072
+ "639_2_T": "kik",
1073
+ "639_2_B": "kik",
1074
+ "name": "Kikuyu, Gikuyu"
1075
+ },
1076
+ {
1077
+ "639_1": "rw",
1078
+ "639_2_T": "kin",
1079
+ "639_2_B": "kin",
1080
+ "name": "Kinyarwanda"
1081
+ },
1082
+ {
1083
+ "639_1": "ky",
1084
+ "639_2_T": "kir",
1085
+ "639_2_B": "kir",
1086
+ "name": "Kirghiz, Kyrgyz"
1087
+ },
1088
+ {
1089
+ "639_2_T": "kmb",
1090
+ "name": "Kimbundu"
1091
+ },
1092
+ {
1093
+ "639_2_T": "kok",
1094
+ "name": "Konkani"
1095
+ },
1096
+ {
1097
+ "639_1": "kv",
1098
+ "639_2_T": "kom",
1099
+ "639_2_B": "kom",
1100
+ "name": "Komi"
1101
+ },
1102
+ {
1103
+ "639_1": "kg",
1104
+ "639_2_T": "kon",
1105
+ "639_2_B": "kon",
1106
+ "name": "Kongo"
1107
+ },
1108
+ {
1109
+ "639_1": "ko",
1110
+ "639_2_T": "kor",
1111
+ "639_2_B": "kor",
1112
+ "name": "Korean"
1113
+ },
1114
+ {
1115
+ "639_2_T": "kos",
1116
+ "name": "Kosraean"
1117
+ },
1118
+ {
1119
+ "639_2_T": "kpe",
1120
+ "name": "Kpelle"
1121
+ },
1122
+ {
1123
+ "639_2_T": "krc",
1124
+ "name": "Karachay-Balkar"
1125
+ },
1126
+ {
1127
+ "639_2_T": "krl",
1128
+ "name": "Karelian"
1129
+ },
1130
+ {
1131
+ "639_2_T": "kro",
1132
+ "name": "Kru languages"
1133
+ },
1134
+ {
1135
+ "639_2_T": "kru",
1136
+ "name": "Kurukh"
1137
+ },
1138
+ {
1139
+ "639_1": "kj",
1140
+ "639_2_T": "kua",
1141
+ "639_2_B": "kua",
1142
+ "name": "Kuanyama, Kwanyama"
1143
+ },
1144
+ {
1145
+ "639_2_T": "kum",
1146
+ "name": "Kumyk"
1147
+ },
1148
+ {
1149
+ "639_1": "ku",
1150
+ "639_2_T": "kur",
1151
+ "639_2_B": "kur",
1152
+ "name": "Kurdish"
1153
+ },
1154
+ {
1155
+ "639_2_T": "kut",
1156
+ "name": "Kutenai"
1157
+ },
1158
+ {
1159
+ "639_2_T": "lad",
1160
+ "name": "Ladino"
1161
+ },
1162
+ {
1163
+ "639_2_T": "lah",
1164
+ "name": "Lahnda"
1165
+ },
1166
+ {
1167
+ "639_2_T": "lam",
1168
+ "name": "Lamba"
1169
+ },
1170
+ {
1171
+ "639_1": "lo",
1172
+ "639_2_T": "lao",
1173
+ "639_2_B": "lao",
1174
+ "name": "Lao"
1175
+ },
1176
+ {
1177
+ "639_1": "la",
1178
+ "639_2_T": "lat",
1179
+ "639_2_B": "lat",
1180
+ "name": "Latin"
1181
+ },
1182
+ {
1183
+ "639_1": "lv",
1184
+ "639_2_T": "lav",
1185
+ "639_2_B": "lav",
1186
+ "name": "Latvian"
1187
+ },
1188
+ {
1189
+ "639_2_T": "lez",
1190
+ "name": "Lezghian"
1191
+ },
1192
+ {
1193
+ "639_1": "li",
1194
+ "639_2_T": "lim",
1195
+ "639_2_B": "lim",
1196
+ "name": "Limburgan, Limburger, Limburgish"
1197
+ },
1198
+ {
1199
+ "639_1": "ln",
1200
+ "639_2_T": "lin",
1201
+ "639_2_B": "lin",
1202
+ "name": "Lingala"
1203
+ },
1204
+ {
1205
+ "639_1": "lt",
1206
+ "639_2_T": "lit",
1207
+ "639_2_B": "lit",
1208
+ "name": "Lithuanian"
1209
+ },
1210
+ {
1211
+ "639_2_T": "lol",
1212
+ "name": "Mongo"
1213
+ },
1214
+ {
1215
+ "639_2_T": "loz",
1216
+ "name": "Lozi"
1217
+ },
1218
+ {
1219
+ "639_1": "lb",
1220
+ "639_2_T": "ltz",
1221
+ "639_2_B": "ltz",
1222
+ "name": "Luxembourgish, Letzeburgesch"
1223
+ },
1224
+ {
1225
+ "639_2_T": "lua",
1226
+ "name": "Luba-Lulua"
1227
+ },
1228
+ {
1229
+ "639_1": "lu",
1230
+ "639_2_T": "lub",
1231
+ "639_2_B": "lub",
1232
+ "name": "Luba-Katanga"
1233
+ },
1234
+ {
1235
+ "639_1": "lg",
1236
+ "639_2_T": "lug",
1237
+ "639_2_B": "lug",
1238
+ "name": "Ganda"
1239
+ },
1240
+ {
1241
+ "639_2_T": "lui",
1242
+ "name": "Luiseno"
1243
+ },
1244
+ {
1245
+ "639_2_T": "lun",
1246
+ "name": "Lunda"
1247
+ },
1248
+ {
1249
+ "639_2_T": "luo",
1250
+ "name": "Luo (Kenya and Tanzania)"
1251
+ },
1252
+ {
1253
+ "639_2_T": "lus",
1254
+ "name": "Lushai"
1255
+ },
1256
+ {
1257
+ "639_2_T": "mad",
1258
+ "name": "Madurese"
1259
+ },
1260
+ {
1261
+ "639_2_T": "mag",
1262
+ "name": "Magahi"
1263
+ },
1264
+ {
1265
+ "639_1": "mh",
1266
+ "639_2_T": "mah",
1267
+ "639_2_B": "mah",
1268
+ "name": "Marshallese"
1269
+ },
1270
+ {
1271
+ "639_2_T": "mai",
1272
+ "name": "Maithili"
1273
+ },
1274
+ {
1275
+ "639_2_T": "mak",
1276
+ "name": "Makasar"
1277
+ },
1278
+ {
1279
+ "639_1": "ml",
1280
+ "639_2_T": "mal",
1281
+ "639_2_B": "mal",
1282
+ "name": "Malayalam"
1283
+ },
1284
+ {
1285
+ "639_2_T": "man",
1286
+ "name": "Mandingo"
1287
+ },
1288
+ {
1289
+ "639_2_T": "map",
1290
+ "name": "Austronesian languages"
1291
+ },
1292
+ {
1293
+ "639_1": "mr",
1294
+ "639_2_T": "mar",
1295
+ "639_2_B": "mar",
1296
+ "name": "Marathi"
1297
+ },
1298
+ {
1299
+ "639_2_T": "mas",
1300
+ "name": "Masai"
1301
+ },
1302
+ {
1303
+ "639_2_T": "mdf",
1304
+ "name": "Moksha"
1305
+ },
1306
+ {
1307
+ "639_2_T": "mdr",
1308
+ "name": "Mandar"
1309
+ },
1310
+ {
1311
+ "639_2_T": "men",
1312
+ "name": "Mende"
1313
+ },
1314
+ {
1315
+ "639_2_T": "mga",
1316
+ "name": "Irish, Middle (900-1200)"
1317
+ },
1318
+ {
1319
+ "639_2_T": "mic",
1320
+ "name": "Mi'kmaq; Micmac"
1321
+ },
1322
+ {
1323
+ "639_2_T": "min",
1324
+ "name": "Minangkabau"
1325
+ },
1326
+ {
1327
+ "639_2_T": "mis",
1328
+ "name": "Uncoded languages"
1329
+ },
1330
+ {
1331
+ "639_1": "mk",
1332
+ "639_2_T": "mkd",
1333
+ "639_2_B": "mac",
1334
+ "name": "Macedonian"
1335
+ },
1336
+ {
1337
+ "639_2_T": "mkh",
1338
+ "name": "Mon-Khmer languages"
1339
+ },
1340
+ {
1341
+ "639_1": "mg",
1342
+ "639_2_T": "mlg",
1343
+ "639_2_B": "mlg",
1344
+ "name": "Malagasy"
1345
+ },
1346
+ {
1347
+ "639_1": "mt",
1348
+ "639_2_T": "mlt",
1349
+ "639_2_B": "mlt",
1350
+ "name": "Maltese"
1351
+ },
1352
+ {
1353
+ "639_2_T": "mnc",
1354
+ "name": "Manchu"
1355
+ },
1356
+ {
1357
+ "639_2_T": "mni",
1358
+ "name": "Manipuri"
1359
+ },
1360
+ {
1361
+ "639_2_T": "mno",
1362
+ "name": "Manobo languages"
1363
+ },
1364
+ {
1365
+ "639_2_T": "moh",
1366
+ "name": "Mohawk"
1367
+ },
1368
+ {
1369
+ "639_1": "mn",
1370
+ "639_2_T": "mon",
1371
+ "639_2_B": "mon",
1372
+ "name": "Mongolian"
1373
+ },
1374
+ {
1375
+ "639_2_T": "mos",
1376
+ "name": "Mossi"
1377
+ },
1378
+ {
1379
+ "639_1": "mi",
1380
+ "639_2_T": "mri",
1381
+ "639_2_B": "mao",
1382
+ "name": "Maori"
1383
+ },
1384
+ {
1385
+ "639_1": "ms",
1386
+ "639_2_T": "msa",
1387
+ "639_2_B": "may",
1388
+ "name": "Malay"
1389
+ },
1390
+ {
1391
+ "639_2_T": "mul",
1392
+ "name": "Multiple languages"
1393
+ },
1394
+ {
1395
+ "639_2_T": "mun",
1396
+ "name": "Munda languages"
1397
+ },
1398
+ {
1399
+ "639_2_T": "mus",
1400
+ "name": "Creek"
1401
+ },
1402
+ {
1403
+ "639_2_T": "mwl",
1404
+ "name": "Mirandese"
1405
+ },
1406
+ {
1407
+ "639_2_T": "mwr",
1408
+ "name": "Marwari"
1409
+ },
1410
+ {
1411
+ "639_1": "my",
1412
+ "639_2_T": "mya",
1413
+ "639_2_B": "bur",
1414
+ "name": "Burmese"
1415
+ },
1416
+ {
1417
+ "639_2_T": "myn",
1418
+ "name": "Mayan languages"
1419
+ },
1420
+ {
1421
+ "639_2_T": "myv",
1422
+ "name": "Erzya"
1423
+ },
1424
+ {
1425
+ "639_2_T": "nah",
1426
+ "name": "Nahuatl languages"
1427
+ },
1428
+ {
1429
+ "639_2_T": "nai",
1430
+ "name": "North American Indian languages"
1431
+ },
1432
+ {
1433
+ "639_2_T": "nap",
1434
+ "name": "Neapolitan"
1435
+ },
1436
+ {
1437
+ "639_1": "na",
1438
+ "639_2_T": "nau",
1439
+ "639_2_B": "nau",
1440
+ "name": "Nauru"
1441
+ },
1442
+ {
1443
+ "639_1": "nv",
1444
+ "639_2_T": "nav",
1445
+ "639_2_B": "nav",
1446
+ "name": "Navajo, Navaho"
1447
+ },
1448
+ {
1449
+ "639_1": "nr",
1450
+ "639_2_T": "nbl",
1451
+ "639_2_B": "nbl",
1452
+ "name": "South Ndebele"
1453
+ },
1454
+ {
1455
+ "639_1": "nd",
1456
+ "639_2_T": "nde",
1457
+ "639_2_B": "nde",
1458
+ "name": "North Ndebele"
1459
+ },
1460
+ {
1461
+ "639_1": "ng",
1462
+ "639_2_T": "ndo",
1463
+ "639_2_B": "ndo",
1464
+ "name": "Ndonga"
1465
+ },
1466
+ {
1467
+ "639_2_T": "nds",
1468
+ "name": "Low German; Low Saxon; German, Low; Saxon, Low"
1469
+ },
1470
+ {
1471
+ "639_1": "ne",
1472
+ "639_2_T": "nep",
1473
+ "639_2_B": "nep",
1474
+ "name": "Nepali"
1475
+ },
1476
+ {
1477
+ "639_2_T": "new",
1478
+ "name": "Nepal Bhasa; Newari"
1479
+ },
1480
+ {
1481
+ "639_2_T": "nia",
1482
+ "name": "Nias"
1483
+ },
1484
+ {
1485
+ "639_2_T": "nic",
1486
+ "name": "Niger-Kordofanian languages"
1487
+ },
1488
+ {
1489
+ "639_2_T": "niu",
1490
+ "name": "Niuean"
1491
+ },
1492
+ {
1493
+ "639_1": "nl",
1494
+ "639_2_T": "nld",
1495
+ "639_2_B": "dut",
1496
+ "name": "Dutch, Flemish"
1497
+ },
1498
+ {
1499
+ "639_1": "nl",
1500
+ "639_2_T": "nld",
1501
+ "639_2_B": "dut",
1502
+ "name": "Dutch; Flemish"
1503
+ },
1504
+ {
1505
+ "639_1": "nn",
1506
+ "639_2_T": "nno",
1507
+ "639_2_B": "nno",
1508
+ "name": "Norwegian Nynorsk"
1509
+ },
1510
+ {
1511
+ "639_1": "nb",
1512
+ "639_2_T": "nob",
1513
+ "639_2_B": "nob",
1514
+ "name": "Norwegian Bokmål"
1515
+ },
1516
+ {
1517
+ "639_2_T": "nog",
1518
+ "name": "Nogai"
1519
+ },
1520
+ {
1521
+ "639_2_T": "non",
1522
+ "name": "Norse, Old"
1523
+ },
1524
+ {
1525
+ "639_1": "no",
1526
+ "639_2_T": "nor",
1527
+ "639_2_B": "nor",
1528
+ "name": "Norwegian"
1529
+ },
1530
+ {
1531
+ "639_2_T": "nqo",
1532
+ "name": "N'Ko"
1533
+ },
1534
+ {
1535
+ "639_2_T": "nso",
1536
+ "name": "Pedi; Sepedi; Northern Sotho"
1537
+ },
1538
+ {
1539
+ "639_2_T": "nub",
1540
+ "name": "Nubian languages"
1541
+ },
1542
+ {
1543
+ "639_2_T": "nwc",
1544
+ "name": "Classical Newari; Old Newari; Classical Nepal Bhasa"
1545
+ },
1546
+ {
1547
+ "639_1": "ny",
1548
+ "639_2_T": "nya",
1549
+ "639_2_B": "nya",
1550
+ "name": "Chichewa, Chewa, Nyanja"
1551
+ },
1552
+ {
1553
+ "639_2_T": "nym",
1554
+ "name": "Nyamwezi"
1555
+ },
1556
+ {
1557
+ "639_2_T": "nyn",
1558
+ "name": "Nyankole"
1559
+ },
1560
+ {
1561
+ "639_2_T": "nyo",
1562
+ "name": "Nyoro"
1563
+ },
1564
+ {
1565
+ "639_2_T": "nzi",
1566
+ "name": "Nzima"
1567
+ },
1568
+ {
1569
+ "639_1": "oc",
1570
+ "639_2_T": "oci",
1571
+ "639_2_B": "oci",
1572
+ "name": "Occitan"
1573
+ },
1574
+ {
1575
+ "639_1": "oj",
1576
+ "639_2_T": "oji",
1577
+ "639_2_B": "oji",
1578
+ "name": "Ojibwa"
1579
+ },
1580
+ {
1581
+ "639_1": "or",
1582
+ "639_2_T": "ori",
1583
+ "639_2_B": "ori",
1584
+ "name": "Oriya"
1585
+ },
1586
+ {
1587
+ "639_1": "om",
1588
+ "639_2_T": "orm",
1589
+ "639_2_B": "orm",
1590
+ "name": "Oromo"
1591
+ },
1592
+ {
1593
+ "639_2_T": "osa",
1594
+ "name": "Osage"
1595
+ },
1596
+ {
1597
+ "639_1": "os",
1598
+ "639_2_T": "oss",
1599
+ "639_2_B": "oss",
1600
+ "name": "Ossetian, Ossetic"
1601
+ },
1602
+ {
1603
+ "639_2_T": "ota",
1604
+ "name": "Turkish, Ottoman (1500-1928)"
1605
+ },
1606
+ {
1607
+ "639_2_T": "oto",
1608
+ "name": "Otomian languages"
1609
+ },
1610
+ {
1611
+ "639_2_T": "paa",
1612
+ "name": "Papuan languages"
1613
+ },
1614
+ {
1615
+ "639_2_T": "pag",
1616
+ "name": "Pangasinan"
1617
+ },
1618
+ {
1619
+ "639_2_T": "pal",
1620
+ "name": "Pahlavi"
1621
+ },
1622
+ {
1623
+ "639_2_T": "pam",
1624
+ "name": "Pampanga; Kapampangan"
1625
+ },
1626
+ {
1627
+ "639_1": "pa",
1628
+ "639_2_T": "pan",
1629
+ "639_2_B": "pan",
1630
+ "name": "Punjabi, Panjabi"
1631
+ },
1632
+ {
1633
+ "639_2_T": "pap",
1634
+ "name": "Papiamento"
1635
+ },
1636
+ {
1637
+ "639_2_T": "pau",
1638
+ "name": "Palauan"
1639
+ },
1640
+ {
1641
+ "639_2_T": "peo",
1642
+ "name": "Persian, Old (ca.600-400 B.C.)"
1643
+ },
1644
+ {
1645
+ "639_2_T": "phi",
1646
+ "name": "Philippine languages"
1647
+ },
1648
+ {
1649
+ "639_2_T": "phn",
1650
+ "name": "Phoenician"
1651
+ },
1652
+ {
1653
+ "639_1": "pi",
1654
+ "639_2_T": "pli",
1655
+ "639_2_B": "pli",
1656
+ "name": "Pali"
1657
+ },
1658
+ {
1659
+ "639_1": "pl",
1660
+ "639_2_T": "pol",
1661
+ "639_2_B": "pol",
1662
+ "name": "Polish"
1663
+ },
1664
+ {
1665
+ "639_2_T": "pon",
1666
+ "name": "Pohnpeian"
1667
+ },
1668
+ {
1669
+ "639_1": "pt",
1670
+ "639_2_T": "por",
1671
+ "639_2_B": "por",
1672
+ "name": "Portuguese"
1673
+ },
1674
+ {
1675
+ "639_2_T": "pra",
1676
+ "name": "Prakrit languages"
1677
+ },
1678
+ {
1679
+ "639_2_T": "pro",
1680
+ "name": "Provençal, Old (to 1500);Occitan, Old (to 1500)"
1681
+ },
1682
+ {
1683
+ "639_1": "ps",
1684
+ "639_2_T": "pus",
1685
+ "639_2_B": "pus",
1686
+ "name": "Pashto, Pushto"
1687
+ },
1688
+ {
1689
+ "639_2_T": "qaa-qtz",
1690
+ "name": "Reserved for local use"
1691
+ },
1692
+ {
1693
+ "639_1": "qu",
1694
+ "639_2_T": "que",
1695
+ "639_2_B": "que",
1696
+ "name": "Quechua"
1697
+ },
1698
+ {
1699
+ "639_2_T": "raj",
1700
+ "name": "Rajasthani"
1701
+ },
1702
+ {
1703
+ "639_2_T": "rap",
1704
+ "name": "Rapanui"
1705
+ },
1706
+ {
1707
+ "639_2_T": "rar",
1708
+ "name": "Rarotongan; Cook Islands Maori"
1709
+ },
1710
+ {
1711
+ "639_2_T": "roa",
1712
+ "name": "Romance languages"
1713
+ },
1714
+ {
1715
+ "639_1": "rm",
1716
+ "639_2_T": "roh",
1717
+ "639_2_B": "roh",
1718
+ "name": "Romansh"
1719
+ },
1720
+ {
1721
+ "639_2_T": "rom",
1722
+ "name": "Romany"
1723
+ },
1724
+ {
1725
+ "639_1": "ro",
1726
+ "639_2_T": "ron",
1727
+ "639_2_B": "rum",
1728
+ "name": "Romanian, Moldavian, Moldovan"
1729
+ },
1730
+ {
1731
+ "639_1": "ro",
1732
+ "639_2_T": "ron",
1733
+ "639_2_B": "rum",
1734
+ "name": "Romanian; Moldavian; Moldovan"
1735
+ },
1736
+ {
1737
+ "639_1": "rn",
1738
+ "639_2_T": "run",
1739
+ "639_2_B": "run",
1740
+ "name": "Rundi"
1741
+ },
1742
+ {
1743
+ "639_2_T": "rup",
1744
+ "name": "Aromanian; Arumanian; Macedo-Romanian"
1745
+ },
1746
+ {
1747
+ "639_1": "ru",
1748
+ "639_2_T": "rus",
1749
+ "639_2_B": "rus",
1750
+ "name": "Russian"
1751
+ },
1752
+ {
1753
+ "639_2_T": "sad",
1754
+ "name": "Sandawe"
1755
+ },
1756
+ {
1757
+ "639_1": "sg",
1758
+ "639_2_T": "sag",
1759
+ "639_2_B": "sag",
1760
+ "name": "Sango"
1761
+ },
1762
+ {
1763
+ "639_2_T": "sah",
1764
+ "name": "Yakut"
1765
+ },
1766
+ {
1767
+ "639_2_T": "sai",
1768
+ "name": "South American Indian languages"
1769
+ },
1770
+ {
1771
+ "639_2_T": "sal",
1772
+ "name": "Salishan languages"
1773
+ },
1774
+ {
1775
+ "639_2_T": "sam",
1776
+ "name": "Samaritan Aramaic"
1777
+ },
1778
+ {
1779
+ "639_1": "sa",
1780
+ "639_2_T": "san",
1781
+ "639_2_B": "san",
1782
+ "name": "Sanskrit"
1783
+ },
1784
+ {
1785
+ "639_2_T": "sas",
1786
+ "name": "Sasak"
1787
+ },
1788
+ {
1789
+ "639_2_T": "sat",
1790
+ "name": "Santali"
1791
+ },
1792
+ {
1793
+ "639_2_T": "scn",
1794
+ "name": "Sicilian"
1795
+ },
1796
+ {
1797
+ "639_2_T": "sco",
1798
+ "name": "Scots"
1799
+ },
1800
+ {
1801
+ "639_2_T": "sel",
1802
+ "name": "Selkup"
1803
+ },
1804
+ {
1805
+ "639_2_T": "sem",
1806
+ "name": "Semitic languages"
1807
+ },
1808
+ {
1809
+ "639_2_T": "sga",
1810
+ "name": "Irish, Old (to 900)"
1811
+ },
1812
+ {
1813
+ "639_2_T": "sgn",
1814
+ "name": "Sign Languages"
1815
+ },
1816
+ {
1817
+ "639_2_T": "shn",
1818
+ "name": "Shan"
1819
+ },
1820
+ {
1821
+ "639_2_T": "sid",
1822
+ "name": "Sidamo"
1823
+ },
1824
+ {
1825
+ "639_1": "si",
1826
+ "639_2_T": "sin",
1827
+ "639_2_B": "sin",
1828
+ "name": "Sinhala, Sinhalese"
1829
+ },
1830
+ {
1831
+ "639_2_T": "sio",
1832
+ "name": "Siouan languages"
1833
+ },
1834
+ {
1835
+ "639_2_T": "sit",
1836
+ "name": "Sino-Tibetan languages"
1837
+ },
1838
+ {
1839
+ "639_2_T": "sla",
1840
+ "name": "Slavic languages"
1841
+ },
1842
+ {
1843
+ "639_1": "sk",
1844
+ "639_2_T": "slk",
1845
+ "639_2_B": "slo",
1846
+ "name": "Slovak"
1847
+ },
1848
+ {
1849
+ "639_1": "sl",
1850
+ "639_2_T": "slv",
1851
+ "639_2_B": "slv",
1852
+ "name": "Slovenian"
1853
+ },
1854
+ {
1855
+ "639_2_T": "sma",
1856
+ "name": "Southern Sami"
1857
+ },
1858
+ {
1859
+ "639_1": "se",
1860
+ "639_2_T": "sme",
1861
+ "639_2_B": "sme",
1862
+ "name": "Northern Sami"
1863
+ },
1864
+ {
1865
+ "639_2_T": "smi",
1866
+ "name": "Sami languages"
1867
+ },
1868
+ {
1869
+ "639_2_T": "smj",
1870
+ "name": "Lule Sami"
1871
+ },
1872
+ {
1873
+ "639_2_T": "smn",
1874
+ "name": "Inari Sami"
1875
+ },
1876
+ {
1877
+ "639_1": "sm",
1878
+ "639_2_T": "smo",
1879
+ "639_2_B": "smo",
1880
+ "name": "Samoan"
1881
+ },
1882
+ {
1883
+ "639_2_T": "sms",
1884
+ "name": "Skolt Sami"
1885
+ },
1886
+ {
1887
+ "639_1": "sn",
1888
+ "639_2_T": "sna",
1889
+ "639_2_B": "sna",
1890
+ "name": "Shona"
1891
+ },
1892
+ {
1893
+ "639_1": "sd",
1894
+ "639_2_T": "snd",
1895
+ "639_2_B": "snd",
1896
+ "name": "Sindhi"
1897
+ },
1898
+ {
1899
+ "639_2_T": "snk",
1900
+ "name": "Soninke"
1901
+ },
1902
+ {
1903
+ "639_2_T": "sog",
1904
+ "name": "Sogdian"
1905
+ },
1906
+ {
1907
+ "639_1": "so",
1908
+ "639_2_T": "som",
1909
+ "639_2_B": "som",
1910
+ "name": "Somali"
1911
+ },
1912
+ {
1913
+ "639_2_T": "son",
1914
+ "name": "Songhai languages"
1915
+ },
1916
+ {
1917
+ "639_1": "st",
1918
+ "639_2_T": "sot",
1919
+ "639_2_B": "sot",
1920
+ "name": "Southern Sotho"
1921
+ },
1922
+ {
1923
+ "639_1": "es",
1924
+ "639_2_T": "spa",
1925
+ "639_2_B": "spa",
1926
+ "name": "Spanish, Castilian"
1927
+ },
1928
+ {
1929
+ "639_1": "sq",
1930
+ "639_2_T": "sqi",
1931
+ "639_2_B": "alb",
1932
+ "name": "Albanian"
1933
+ },
1934
+ {
1935
+ "639_1": "sc",
1936
+ "639_2_T": "srd",
1937
+ "639_2_B": "srd",
1938
+ "name": "Sardinian"
1939
+ },
1940
+ {
1941
+ "639_2_T": "srn",
1942
+ "name": "Sranan Tongo"
1943
+ },
1944
+ {
1945
+ "639_1": "sr",
1946
+ "639_2_T": "srp",
1947
+ "639_2_B": "srp",
1948
+ "name": "Serbian"
1949
+ },
1950
+ {
1951
+ "639_2_T": "srr",
1952
+ "name": "Serer"
1953
+ },
1954
+ {
1955
+ "639_2_T": "ssa",
1956
+ "name": "Nilo-Saharan languages"
1957
+ },
1958
+ {
1959
+ "639_1": "ss",
1960
+ "639_2_T": "ssw",
1961
+ "639_2_B": "ssw",
1962
+ "name": "Swati"
1963
+ },
1964
+ {
1965
+ "639_2_T": "suk",
1966
+ "name": "Sukuma"
1967
+ },
1968
+ {
1969
+ "639_1": "su",
1970
+ "639_2_T": "sun",
1971
+ "639_2_B": "sun",
1972
+ "name": "Sundanese"
1973
+ },
1974
+ {
1975
+ "639_2_T": "sus",
1976
+ "name": "Susu"
1977
+ },
1978
+ {
1979
+ "639_2_T": "sux",
1980
+ "name": "Sumerian"
1981
+ },
1982
+ {
1983
+ "639_1": "sw",
1984
+ "639_2_T": "swa",
1985
+ "639_2_B": "swa",
1986
+ "name": "Swahili"
1987
+ },
1988
+ {
1989
+ "639_1": "sv",
1990
+ "639_2_T": "swe",
1991
+ "639_2_B": "swe",
1992
+ "name": "Swedish"
1993
+ },
1994
+ {
1995
+ "639_2_T": "syc",
1996
+ "name": "Classical Syriac"
1997
+ },
1998
+ {
1999
+ "639_2_T": "syr",
2000
+ "name": "Syriac"
2001
+ },
2002
+ {
2003
+ "639_1": "ty",
2004
+ "639_2_T": "tah",
2005
+ "639_2_B": "tah",
2006
+ "name": "Tahitian"
2007
+ },
2008
+ {
2009
+ "639_2_T": "tai",
2010
+ "name": "Tai languages"
2011
+ },
2012
+ {
2013
+ "639_1": "ta",
2014
+ "639_2_T": "tam",
2015
+ "639_2_B": "tam",
2016
+ "name": "Tamil"
2017
+ },
2018
+ {
2019
+ "639_1": "tt",
2020
+ "639_2_T": "tat",
2021
+ "639_2_B": "tat",
2022
+ "name": "Tatar"
2023
+ },
2024
+ {
2025
+ "639_1": "te",
2026
+ "639_2_T": "tel",
2027
+ "639_2_B": "tel",
2028
+ "name": "Telugu"
2029
+ },
2030
+ {
2031
+ "639_2_T": "tem",
2032
+ "name": "Timne"
2033
+ },
2034
+ {
2035
+ "639_2_T": "ter",
2036
+ "name": "Tereno"
2037
+ },
2038
+ {
2039
+ "639_2_T": "tet",
2040
+ "name": "Tetum"
2041
+ },
2042
+ {
2043
+ "639_1": "tg",
2044
+ "639_2_T": "tgk",
2045
+ "639_2_B": "tgk",
2046
+ "name": "Tajik"
2047
+ },
2048
+ {
2049
+ "639_1": "tl",
2050
+ "639_2_T": "tgl",
2051
+ "639_2_B": "tgl",
2052
+ "name": "Tagalog"
2053
+ },
2054
+ {
2055
+ "639_1": "th",
2056
+ "639_2_T": "tha",
2057
+ "639_2_B": "tha",
2058
+ "name": "Thai"
2059
+ },
2060
+ {
2061
+ "639_2_T": "tig",
2062
+ "name": "Tigre"
2063
+ },
2064
+ {
2065
+ "639_1": "ti",
2066
+ "639_2_T": "tir",
2067
+ "639_2_B": "tir",
2068
+ "name": "Tigrinya"
2069
+ },
2070
+ {
2071
+ "639_2_T": "tiv",
2072
+ "name": "Tiv"
2073
+ },
2074
+ {
2075
+ "639_2_T": "tkl",
2076
+ "name": "Tokelau"
2077
+ },
2078
+ {
2079
+ "639_2_T": "tlh",
2080
+ "name": "Klingon; tlhIngan-Hol"
2081
+ },
2082
+ {
2083
+ "639_2_T": "tli",
2084
+ "name": "Tlingit"
2085
+ },
2086
+ {
2087
+ "639_2_T": "tmh",
2088
+ "name": "Tamashek"
2089
+ },
2090
+ {
2091
+ "639_2_T": "tog",
2092
+ "name": "Tonga (Nyasa)"
2093
+ },
2094
+ {
2095
+ "639_1": "to",
2096
+ "639_2_T": "ton",
2097
+ "639_2_B": "ton",
2098
+ "name": "Tonga (Tonga Islands)"
2099
+ },
2100
+ {
2101
+ "639_2_T": "tpi",
2102
+ "name": "Tok Pisin"
2103
+ },
2104
+ {
2105
+ "639_2_T": "tsi",
2106
+ "name": "Tsimshian"
2107
+ },
2108
+ {
2109
+ "639_1": "tn",
2110
+ "639_2_T": "tsn",
2111
+ "639_2_B": "tsn",
2112
+ "name": "Tswana"
2113
+ },
2114
+ {
2115
+ "639_1": "ts",
2116
+ "639_2_T": "tso",
2117
+ "639_2_B": "tso",
2118
+ "name": "Tsonga"
2119
+ },
2120
+ {
2121
+ "639_1": "tk",
2122
+ "639_2_T": "tuk",
2123
+ "639_2_B": "tuk",
2124
+ "name": "Turkmen"
2125
+ },
2126
+ {
2127
+ "639_2_T": "tum",
2128
+ "name": "Tumbuka"
2129
+ },
2130
+ {
2131
+ "639_2_T": "tup",
2132
+ "name": "Tupi languages"
2133
+ },
2134
+ {
2135
+ "639_1": "tr",
2136
+ "639_2_T": "tur",
2137
+ "639_2_B": "tur",
2138
+ "name": "Turkish"
2139
+ },
2140
+ {
2141
+ "639_2_T": "tut",
2142
+ "name": "Altaic languages"
2143
+ },
2144
+ {
2145
+ "639_2_T": "tvl",
2146
+ "name": "Tuvalu"
2147
+ },
2148
+ {
2149
+ "639_1": "tw",
2150
+ "639_2_T": "twi",
2151
+ "639_2_B": "twi",
2152
+ "name": "Twi"
2153
+ },
2154
+ {
2155
+ "639_2_T": "tyv",
2156
+ "name": "Tuvinian"
2157
+ },
2158
+ {
2159
+ "639_2_T": "udm",
2160
+ "name": "Udmurt"
2161
+ },
2162
+ {
2163
+ "639_2_T": "uga",
2164
+ "name": "Ugaritic"
2165
+ },
2166
+ {
2167
+ "639_1": "ug",
2168
+ "639_2_T": "uig",
2169
+ "639_2_B": "uig",
2170
+ "name": "Uighur, Uyghur"
2171
+ },
2172
+ {
2173
+ "639_1": "uk",
2174
+ "639_2_T": "ukr",
2175
+ "639_2_B": "ukr",
2176
+ "name": "Ukrainian"
2177
+ },
2178
+ {
2179
+ "639_2_T": "umb",
2180
+ "name": "Umbundu"
2181
+ },
2182
+ {
2183
+ "639_2_T": "und",
2184
+ "name": "Undetermined"
2185
+ },
2186
+ {
2187
+ "639_1": "ur",
2188
+ "639_2_T": "urd",
2189
+ "639_2_B": "urd",
2190
+ "name": "Urdu"
2191
+ },
2192
+ {
2193
+ "639_1": "uz",
2194
+ "639_2_T": "uzb",
2195
+ "639_2_B": "uzb",
2196
+ "name": "Uzbek"
2197
+ },
2198
+ {
2199
+ "639_2_T": "vai",
2200
+ "name": "Vai"
2201
+ },
2202
+ {
2203
+ "639_1": "ve",
2204
+ "639_2_T": "ven",
2205
+ "639_2_B": "ven",
2206
+ "name": "Venda"
2207
+ },
2208
+ {
2209
+ "639_1": "vi",
2210
+ "639_2_T": "vie",
2211
+ "639_2_B": "vie",
2212
+ "name": "Vietnamese"
2213
+ },
2214
+ {
2215
+ "639_1": "vo",
2216
+ "639_2_T": "vol",
2217
+ "639_2_B": "vol",
2218
+ "name": "Volapük"
2219
+ },
2220
+ {
2221
+ "639_2_T": "vot",
2222
+ "name": "Votic"
2223
+ },
2224
+ {
2225
+ "639_2_T": "wak",
2226
+ "name": "Wakashan languages"
2227
+ },
2228
+ {
2229
+ "639_2_T": "wal",
2230
+ "name": "Wolaitta; Wolaytta"
2231
+ },
2232
+ {
2233
+ "639_2_T": "war",
2234
+ "name": "Waray"
2235
+ },
2236
+ {
2237
+ "639_2_T": "was",
2238
+ "name": "Washo"
2239
+ },
2240
+ {
2241
+ "639_2_T": "wen",
2242
+ "name": "Sorbian languages"
2243
+ },
2244
+ {
2245
+ "639_1": "wa",
2246
+ "639_2_T": "wln",
2247
+ "639_2_B": "wln",
2248
+ "name": "Walloon"
2249
+ },
2250
+ {
2251
+ "639_1": "wo",
2252
+ "639_2_T": "wol",
2253
+ "639_2_B": "wol",
2254
+ "name": "Wolof"
2255
+ },
2256
+ {
2257
+ "639_2_T": "xal",
2258
+ "name": "Kalmyk; Oirat"
2259
+ },
2260
+ {
2261
+ "639_1": "xh",
2262
+ "639_2_T": "xho",
2263
+ "639_2_B": "xho",
2264
+ "name": "Xhosa"
2265
+ },
2266
+ {
2267
+ "639_2_T": "yao",
2268
+ "name": "Yao"
2269
+ },
2270
+ {
2271
+ "639_2_T": "yap",
2272
+ "name": "Yapese"
2273
+ },
2274
+ {
2275
+ "639_1": "yi",
2276
+ "639_2_T": "yid",
2277
+ "639_2_B": "yid",
2278
+ "name": "Yiddish"
2279
+ },
2280
+ {
2281
+ "639_1": "yo",
2282
+ "639_2_T": "yor",
2283
+ "639_2_B": "yor",
2284
+ "name": "Yoruba"
2285
+ },
2286
+ {
2287
+ "639_2_T": "ypk",
2288
+ "name": "Yupik languages"
2289
+ },
2290
+ {
2291
+ "639_2_T": "zap",
2292
+ "name": "Zapotec"
2293
+ },
2294
+ {
2295
+ "639_2_T": "zbl",
2296
+ "name": "Blissymbols; Blissymbolics; Bliss"
2297
+ },
2298
+ {
2299
+ "639_2_T": "zen",
2300
+ "name": "Zenaga"
2301
+ },
2302
+ {
2303
+ "639_2_T": "zgh",
2304
+ "name": "Standard Moroccan Tamazight"
2305
+ },
2306
+ {
2307
+ "639_1": "za",
2308
+ "639_2_T": "zha",
2309
+ "639_2_B": "zha",
2310
+ "name": "Zhuang, Chuang"
2311
+ },
2312
+ {
2313
+ "639_1": "zh",
2314
+ "639_2_T": "zho",
2315
+ "639_2_B": "chi",
2316
+ "name": "Chinese"
2317
+ },
2318
+ {
2319
+ "639_2_T": "znd",
2320
+ "name": "Zande languages"
2321
+ },
2322
+ {
2323
+ "639_1": "zu",
2324
+ "639_2_T": "zul",
2325
+ "639_2_B": "zul",
2326
+ "name": "Zulu"
2327
+ },
2328
+ {
2329
+ "639_2_T": "zun",
2330
+ "name": "Zuni"
2331
+ },
2332
+ {
2333
+ "639_2_T": "zxx",
2334
+ "name": "No linguistic content; Not applicable"
2335
+ },
2336
+ {
2337
+ "639_2_T": "zza",
2338
+ "name": "Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki"
2339
+ }
2340
+ ];
2341
+
2342
+
2343
+ return {
2344
+ /**
2345
+ * @param code country code corresponding to alpha2, alpha3 or numeric3
2346
+ * @returns {name: String, 639 codes: String}
2347
+ */
2348
+ lookup: function (code) {
2349
+ if (!code) {
2350
+ return;
2351
+ }
2352
+ const codeLower = String(code).toLowerCase();
2353
+ for (let i = 0; i < codes.length; i++) {
2354
+ const codeMap = codes[i];
2355
+
2356
+ if (codeLower === codeMap["639_1"] || codeLower === codeMap["639_2_T"] || codeLower === codeMap["639_2_B"]) {
2357
+ return codeMap;
2358
+ }
2359
+ }
2360
+ }
2361
+ }
2362
+ }());
js/randojs-2.0.0.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // https://github.com/nastyox/Rando.js
2
+ function rando(a,b,e){var g=function(f){return"undefined"===typeof f},k=function(f){return"number"===typeof f&&!isNaN(f)},d=function(f){return!g(f)&&null!==f&&f.constructor===Array},c=function(){try{for(var f,q=[],r;30>(r="."+q.join("")).length;){f=(window.crypto||window.msCrypto).getRandomValues(new Uint32Array(5));for(var p=0;p<f.length;p++){var t=4E9>f[p]?f[p].toString().slice(1):"";0<t.length&&(q[q.length]=t)}}return Number(r)}catch(v){return Math.random()}};try{if(null!==a&&null!==b&&null!==
3
+ e){if(g(a))return c();if(window.jQuery&&a instanceof jQuery&&g(b)){if(0==a.length)return!1;var n=rando(0,a.length-1);return{index:n,value:a.eq(n)}}if(k(a)&&k(b)&&"string"===typeof e&&"float"==e.toLowerCase().trim()){if(a>b){var m=b;b=a;a=m}return c()*(b-a)+a}if(d(a)&&0<a.length&&g(b)){var l=c()*a.length<<0;return{index:l,value:a[l]}}if("object"===typeof a&&g(b)){l=a;var h=Object.keys(l);if(0<h.length){var u=h[h.length*c()<<0];return{key:u,value:l[u]}}}if((!0===a&&!1===b||!1===a&&!0===b)&&g(e))return.5>
4
+ rando();if(k(a)&&g(b))return 0<=a?rando(0,a):rando(a,0);if(k(a)&&"string"===typeof b&&"float"==b.toLowerCase().trim()&&g(e))return 0<=a?rando(0,a,"float"):rando(a,0,"float");if(k(a)&&k(b)&&g(e))return a>b&&(m=b,b=a,a=m),a=Math.floor(a),b=Math.floor(b),Math.floor(c()*(b-a+1)+a);if("string"===typeof a&&0<a.length&&g(b))return a.charAt(rando(0,a.length-1))}return!1}catch(f){return!1}}
5
+ function randoSequence(a,b){var e=function(h){return"undefined"===typeof h},g=function(h){return"number"===typeof h&&!isNaN(h)},k=function(h){return!e(h)&&null!==h&&h.constructor===Array};try{if(e(a)||null===a||null===b)return!1;var d=[];if(window.jQuery&&a instanceof jQuery&&e(b)){if(0<a.length){d=randoSequence(0,a.length-1);for(var c=0;c<d.length;c++)d[c]={index:d[c],value:a.eq(d[c])}}return d}if(e(b))if(k(a)&&e(b))for(c=0;c<a.length;c++)d[d.length]={index:c,value:a[c]};else if("object"===typeof a&&
6
+ e(b))for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(d[d.length]={key:n,value:a[n]});else if("string"===typeof a&&e(b))for(c=0;c<a.length;c++)d[d.length]=a.charAt(c);else return g(a)&&e(b)?0<=a?randoSequence(0,a):randoSequence(a,0):!1;else{if(!g(a)||!g(b)||0<a%1||0<b%1)return!1;if(a>b){var m=b;b=a;a=m}for(c=a;c<=b;c++)d[d.length]=c}for(c=d.length-1;0<c;c--){var l=rando(c);m=d[c];d[c]=d[l];d[l]=m}return d}catch(h){return!1}};
js/shared.js ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Shared methods for normal & bulk pages.
3
+ */
4
+ const shared = (function () {
5
+ 'use strict';
6
+
7
+ const patterns = {
8
+ video_id: [
9
+ /(?:http[s]?:\/\/)?(?:\w+\.)?youtube.com\/watch\?v=([\w_-]+)(?:[\/&].*)?/i,
10
+ /(?:http[s]?:\/\/)?(?:\w+\.)?youtube.com\/(?:v|embed|shorts|video|watch|live)\/([\w_-]+)(?:[\/&].*)?/i,
11
+ /(?:http[s]?:\/\/)?youtu.be\/([\w_-]+)(?:\?.*)?/i,
12
+ /(?:http[s]?:\/\/)?filmot.com\/video\/([\w_-]+)(?:[?\/&].*)?/i,
13
+ /(?:http[s]?:\/\/)?filmot.com\/sidebyside\/([\w_-]+)(?:[?\/&].*)?/i,
14
+ /^([\w-]{11})$/i
15
+ ],
16
+ playlist_id: [
17
+ /(?:http[s]?:\/\/)?(?:\w+\.)?youtube.com\/playlist\?list=([\w_-]+)(?:&.*)?/i,
18
+ /^((UU|UUSH|PL|FL|SP|OLAK)[A-Za-z0-9_-]+)$/i
19
+ ],
20
+ channel_id: [
21
+ /(?:http[s]?:\/\/)?(?:\w+\.)?youtube.com\/channel\/([\w_-]+)(?:\?.*)?/i,
22
+ /(?:http[s]?:\/\/)?filmot.com\/channel\/([\w_-]+)(?:[?\/&].*)?/i,
23
+ /^((UC|SC)[\w-]{22})$/i
24
+ ],
25
+ channel_user: [
26
+ /(?:http[s]?:\/\/)?(?:\w+\.)?youtube.com\/user\/([\w_-]+)(?:\?.*)?/i
27
+ ],
28
+ channel_handle: [
29
+ /(?:http[s]?:\/\/)?(?:\w+\.)?youtube.com\/@([^\/?]+)(?:\?.*)?/i,
30
+ ],
31
+ channel_custom: [
32
+ /(?:http[s]?:\/\/)?(?:\w+\.)?youtube.com\/c\/([^\/?]+)(?:\?.*)?/i,
33
+ /(?:http[s]?:\/\/)?(?:\w+\.)?youtube.com\/([^\/?]+)(?:\?.*)?/i
34
+ ]
35
+ };
36
+
37
+ return {
38
+ /**
39
+ * Safely get nested object property or returns null
40
+ */
41
+ idx: (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o),
42
+
43
+ determineInput: function (value) {
44
+ value = decodeURIComponent(value);
45
+ const parsed = {
46
+ type: 'unknown',
47
+ mayHideOthers: true,
48
+ original: value
49
+ };
50
+ for (let type in patterns) {
51
+ for (let i = 0; i < patterns[type].length; i++) {
52
+ const regex = patterns[type][i];
53
+ const result = regex.exec(value);
54
+
55
+ if (result) {
56
+ parsed.type = type;
57
+ parsed.value = result[1];
58
+ return parsed;
59
+ }
60
+ }
61
+ }
62
+ return parsed;
63
+ },
64
+
65
+ // https://wiki.archiveteam.org/index.php/YouTube/Technical_details
66
+ isValidVideoId: function (videoId) {
67
+ return videoId !== undefined && videoId !== null &&
68
+ String(videoId).match(/[A-Za-z0-9_-]{10}[AEIMQUYcgkosw048]/);
69
+ },
70
+
71
+ isValidChannelId: function (channelId) {
72
+ return channelId !== undefined && channelId !== null &&
73
+ String(channelId).match(/[A-Za-z0-9_-]{21}[AQgw]/);
74
+ },
75
+
76
+ parseQuery: function (queryString) {
77
+ const query = {};
78
+ const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
79
+ for (let i = 0; i < pairs.length; i++) {
80
+ let pair = pairs[i].split('=');
81
+ query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
82
+ }
83
+ return query;
84
+ },
85
+
86
+ formatDuration: function (duration, includeMs, ignoreTime) {
87
+ const years = duration.years();
88
+ const months = duration.months();
89
+ const days = duration.days();
90
+ const hours = duration.hours();
91
+ const minutes = duration.minutes();
92
+ const seconds = duration.seconds();
93
+ const millis = duration.milliseconds();
94
+ const format = [
95
+ (years > 0 ? years + " years" : ""),
96
+ (months > 0 ? months + " months" : ""),
97
+ (days > 0 ? days + " days" : ""),
98
+ (!ignoreTime && hours > 0 ? hours + "h" : ""),
99
+ (!ignoreTime && minutes > 0 ? minutes + "m" : ""),
100
+ (!ignoreTime && seconds > 0 ? seconds + "s" : ""),
101
+ includeMs ? (millis > 0 ? millis + "ms" : "") : ""
102
+ ].join(" ");
103
+
104
+ if (format.trim() === "") {
105
+ return "0s";
106
+ }
107
+
108
+ return format;
109
+ },
110
+
111
+ sortObject: function (unordered, sortArrays = false) {
112
+ if (!unordered || typeof unordered !== 'object') {
113
+ return unordered;
114
+ }
115
+
116
+ if (Array.isArray(unordered)) {
117
+ const newArr = unordered.map((item) => this.sortObject(item, sortArrays));
118
+ if (sortArrays) {
119
+ newArr.sort();
120
+ }
121
+ return newArr;
122
+ }
123
+
124
+ const ordered = {};
125
+ Object.keys(unordered)
126
+ .sort()
127
+ .forEach((key) => {
128
+ ordered[key] = this.sortObject(unordered[key], sortArrays);
129
+ });
130
+ return ordered;
131
+ },
132
+
133
+ safeFileName: function (fileName) {
134
+ return fileName.replace(/[<>:"\/\\|?*]+/g, '');
135
+ },
136
+
137
+ randomFromList(list) {
138
+ let index = 0;
139
+ for (let i = 0; i < 5; i++) {
140
+ index = rando(0, list.length - 1);
141
+ }
142
+ return list[index];
143
+ }
144
+ }
145
+ })();
js/youtube-api-v3.js ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * YouTube API v3
3
+ * Reference documentation:
4
+ * https://developers.google.com/youtube/v3/docs/
5
+ *
6
+ * youtube.ajax("videos", {
7
+ * part: 'snippet'
8
+ * }).done(function (res) {
9
+ *
10
+ * }).fail(function (err) {
11
+ *
12
+ * });
13
+ *
14
+ * @requires jquery
15
+ * @author mattwright324
16
+ */
17
+ const youtube = (function ($) {
18
+ 'use strict';
19
+
20
+ function makeStr(length) {
21
+ let result = '';
22
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
23
+ const charactersLength = characters.length;
24
+ let counter = 0;
25
+ while (counter < length) {
26
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
27
+ counter += 1;
28
+ }
29
+ return result;
30
+ }
31
+
32
+ const tempId = localStorage.getItem("tempId") || makeStr(40);
33
+ localStorage.setItem("tempId", tempId);
34
+
35
+ let defaultKey = "";
36
+ let currentKey = "";
37
+
38
+ return {
39
+ setDefaultKey: function (key) {
40
+ defaultKey = key;
41
+ this.setKey(key);
42
+ },
43
+ getDefaultKey: function () {
44
+ return defaultKey;
45
+ },
46
+ setKey: function (key) {
47
+ if (defaultKey === "") {
48
+ this.setDefaultKey(key);
49
+ }
50
+ currentKey = key;
51
+ },
52
+ getKey: function () {
53
+ return currentKey;
54
+ },
55
+ ajax: function (type, data) {
56
+ if (!defaultKey && defaultKey === "" && !currentKey && currentKey === "") {
57
+ console.error("YouTube API Key Missing");
58
+ } else {
59
+ return $.ajax({
60
+ cache: false,
61
+ data: $.extend({key: currentKey, quotaUser: tempId}, data),
62
+ dataType: "json",
63
+ type: "GET",
64
+ timeout: 5000,
65
+ url: "https://www.googleapis.com/youtube/v3/" + type
66
+ });
67
+ }
68
+ }
69
+ };
70
+ }($));
71
+ youtube.setDefaultKey(atob('QUl6YVN5QVNUTVFjay1qdHRGOHF5OXJ0RW50MUh5RVl3NUFtaEU4'));
js/youtube-metadata-bulk.js ADDED
The diff for this file is too large to render. See raw diff
 
js/youtube-metadata-examples.js ADDED
The diff for this file is too large to render. See raw diff
 
js/youtube-metadata.js ADDED
@@ -0,0 +1,1868 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * YouTube Metadata
3
+ *
4
+ * Grab everything publicly available from the YouTube API.
5
+ *
6
+ * @requires jquery
7
+ * @author mattwright324
8
+ */
9
+ (function () {
10
+ 'use strict';
11
+
12
+ const elements = {};
13
+ const controls = {};
14
+ let exportData = {};
15
+
16
+ const delaySubmitKey = "delaySubmitNormal";
17
+ const can = {
18
+ submit: true,
19
+ };
20
+
21
+ const delay5Sec = 5;
22
+ const delay5SecMs = delay5Sec * 1000;
23
+
24
+ function countdown(key, control, delay, flag) {
25
+ control.addClass("loading").addClass("disabled");
26
+ can[flag] = false;
27
+
28
+ let value = localStorage.getItem(key);
29
+ if (!moment(value).isValid()) {
30
+ console.warn('value for %s was not a valid date, resetting to now', key);
31
+ localStorage.setItem(key, new Date());
32
+ value = localStorage.getItem(key);
33
+ }
34
+ if (moment(value).isAfter(moment())) {
35
+ console.warn('value for %s was set in the future, resetting to now', key);
36
+ localStorage.setItem(key, new Date());
37
+ value = localStorage.getItem(key);
38
+ }
39
+ let count = (delay - moment().diff(value)) / 1000;
40
+ control.find(".countdown").text(Math.trunc(count));
41
+
42
+ function c(control, count) {
43
+ if (count <= 1) {
44
+ control.removeClass("loading").removeClass("disabled");
45
+ control.find(".countdown").text("");
46
+
47
+ can[flag] = true;
48
+ } else {
49
+ control.find(".countdown").text(Math.trunc(count));
50
+ setTimeout(function () {
51
+ c(control, count - 1)
52
+ }, 1000);
53
+ }
54
+ }
55
+
56
+ setTimeout(function () {
57
+ c(control, count)
58
+ }, 1000);
59
+ }
60
+
61
+ function countdownCheck(key, control, delayMs, flag) {
62
+ const value = localStorage.getItem(key);
63
+ if (key in localStorage && moment(value).isValid() && moment().diff(value) < delayMs) {
64
+ countdown(key, control, delayMs, flag);
65
+ } else {
66
+ control.removeClass("loading").removeClass("disabled");
67
+ control.find(".countdown").text("");
68
+ }
69
+ }
70
+ function getSuggestedHtml(parsedInput, fullJson, jsonType, filmot) {
71
+ const suggested = getSuggestedLinks(parsedInput, fullJson, jsonType, filmot);
72
+ suggested.sort((a, b) => (a.text > b.text) ? 1 : -1)
73
+ const html = [];
74
+ for (let i = 0; i < suggested.length; i++) {
75
+ const link = suggested[i];
76
+ html.push("<li><a target='_blank' href='" + link.url + "'>" + link.text + "</a></li>")
77
+ }
78
+ return "<ul>" + html.join("") + "</ul>";
79
+ }
80
+
81
+ function getConvertTime(date) {
82
+ const momentTime = moment(date).utc();
83
+
84
+ return " (<a target='_blank' href='https://www.timeanddate.com/worldclock/converter.html?iso=" +
85
+ momentTime.format("YYYYMMDDTHHmmss") + "'>convert</a>)"
86
+ }
87
+
88
+ function getSuggestedLinks(parsedInput, fullJson, jsonType, filmot) {
89
+ const data = {}
90
+ if (parsedInput) {
91
+ data[parsedInput.type] = parsedInput.value;
92
+ } else if (fullJson) {
93
+ if (jsonType === "video") {
94
+ data["video_id"] = fullJson.id;
95
+ data["video_title"] = shared.idx(["snippet", "title"], fullJson);
96
+ data["test"] = "test";
97
+ } else if (jsonType === "playlist") {
98
+ data["playlist_id"] = fullJson.id;
99
+ data["playlist_title"] = shared.idx(["snippet", "title"], fullJson);
100
+ } else if (jsonType === "channel") {
101
+ data["channel_id"] = fullJson.id;
102
+ data["channel_title"] = shared.idx(["snippet", "title"], fullJson);
103
+
104
+ const custom = shared.idx(["snippet", "customUrl"], fullJson);
105
+ if (custom) {
106
+ data["channel_custom"] = custom;
107
+ }
108
+ }
109
+ } else if (filmot) {
110
+ data["video_title"] = filmot.title;
111
+ data["channel_id"] = filmot.channelid;
112
+ data["channel_title"] = filmot.channelname;
113
+ }
114
+ console.log(data);
115
+
116
+ function encode(text) {
117
+ return encodeURIComponent(text).replace(/'/g, "%27");
118
+ }
119
+
120
+ const suggestions = [];
121
+ if (data.hasOwnProperty("video_title")) {
122
+ suggestions.push({
123
+ url: "https://www.google.com/search?q=\"" + encode(data.video_title) + "\"",
124
+ text: "Google - \"" + data.video_title + "\""
125
+ });
126
+ suggestions.push({
127
+ url: "https://archive.org/search.php?query=" + encode(data.video_title),
128
+ text: "Archive.org (search) - " + data.video_title
129
+ });
130
+ }
131
+ if (data.hasOwnProperty("video_id")) {
132
+ suggestions.push({
133
+ url: "https://www.google.com/search?q=\"" + data.video_id + "\"",
134
+ text: "Google - \"" + data.video_id + "\""
135
+ });
136
+ suggestions.push({
137
+ url: "https://web.archive.org/web/*/https://www.youtube.com/watch?v=" + data.video_id,
138
+ text: "Archive.org (web) - https://www.youtube.com/watch?v=" + data.video_id
139
+ });
140
+ suggestions.push({
141
+ url: "https://archive.org/details/youtube-" + data.video_id,
142
+ text: "Archive.org (details) - youtube-" + data.video_id
143
+ });
144
+ suggestions.push({
145
+ url: "https://web.archive.org/web/2/http://wayback-fakeurl.archive.org/yt/" + data.video_id,
146
+ text: "Archive.org (direct video wayback-header) - " + data.video_id
147
+ });
148
+ suggestions.push({
149
+ url: "https://web.archive.org/web/2oe_/http://wayback-fakeurl.archive.org/yt/" + data.video_id,
150
+ text: "Archive.org (direct video raw) - " + data.video_id
151
+ });
152
+ suggestions.push({
153
+ url: "https://filmot.com/video/" + data.video_id,
154
+ text: "Filmot.com - https://filmot.com/video/" + data.video_id
155
+ });
156
+ suggestions.push({
157
+ url: "https://ghostarchive.org/varchive/" + data.video_id,
158
+ text: "GhostArchive.org - " + data.video_id
159
+ });
160
+ // suggestions.push({
161
+ // url: "https://www.youtuberecover.com/watch?v=" + data.video_id,
162
+ // text: "YouTube Recover - " + data.video_id
163
+ // });
164
+ suggestions.push({
165
+ url: "https://hobune.stream/videos/" + data.video_id,
166
+ text: "Hobune Archive - " + data.video_id
167
+ });
168
+ }
169
+ if (data.hasOwnProperty("playlist_title")) {
170
+ suggestions.push({
171
+ url: "https://www.google.com/search?q=\"" + encode(data.playlist_title) + "\"",
172
+ text: "Google - \"" + data.playlist_title + "\""
173
+ });
174
+ }
175
+ if (data.hasOwnProperty("playlist_id")) {
176
+ suggestions.push({
177
+ url: "https://www.google.com/search?q=\"" + data.playlist_id + "\"",
178
+ text: "Google - \"" + data.playlist_id + "\""
179
+ });
180
+ suggestions.push({
181
+ url: "https://web.archive.org/web/*/https://www.youtube.com/playlist?list=" + data.playlist_id,
182
+ text: "Archive.org - https://www.youtube.com/playlist?list=" + data.playlist_id
183
+ });
184
+ }
185
+ if (data.hasOwnProperty("channel_title")) {
186
+ suggestions.push({
187
+ url: "https://www.google.com/search?q=\"" + encode(data.channel_title) + "\"",
188
+ text: "Google - \"" + data.channel_title + "\""
189
+ });
190
+ suggestions.push({
191
+ url: "https://archive.org/search.php?query=creator%3A%22" + encode(data.channel_title) + "%22",
192
+ text: "Archive.org (search) - creator:\"" + data.channel_title + "\""
193
+ });
194
+ suggestions.push({
195
+ url: "https://archive.org/search.php?query=subject%3A%22" + encode(data.channel_title) + "%22",
196
+ text: "Archive.org (search) - subject:\"" + data.channel_title + "\""
197
+ });
198
+ suggestions.push({
199
+ url: "https://archive.org/search.php?query=" + encode(data.channel_title),
200
+ text: "Archive.org (search) - " + data.channel_title
201
+ });
202
+ }
203
+ if (data.hasOwnProperty("channel_user")) {
204
+ // this can only happen if /user/ no longer exists on submit
205
+ suggestions.push({
206
+ url: "https://www.google.com/search?q=\"" + data.channel_user + "\"",
207
+ text: "Google - \"" + data.channel_user + "\""
208
+ });
209
+ suggestions.push({
210
+ url: "https://web.archive.org/web/*/https://www.youtube.com/user/" + data.channel_user,
211
+ text: "Archive.org - https://www.youtube.com/user/" + data.channel_user
212
+ });
213
+ suggestions.push({
214
+ url: "https://archive.org/search.php?query=subject%3A%22" + encode(data.channel_user) + "%22",
215
+ text: "Archive.org (search) - subject:\"" + data.channel_user + "\""
216
+ });
217
+ suggestions.push({
218
+ url: "https://archive.org/search.php?query=" + data.channel_user,
219
+ text: "Archive.org (search) - " + data.channel_user
220
+ });
221
+ }
222
+ if (data.hasOwnProperty("channel_custom")) {
223
+ // this can only happen in channel-more if user has custom value
224
+ suggestions.push({
225
+ url: "https://www.google.com/search?q=\"" + data.channel_custom + "\"",
226
+ text: "Google - \"" + data.channel_custom + "\""
227
+ });
228
+ if (data.channel_custom.startsWith('@')) {
229
+ suggestions.push({
230
+ url: "https://web.archive.org/web/*/https://www.youtube.com/" + data.channel_custom,
231
+ text: "Archive.org - https://www.youtube.com/" + data.channel_custom
232
+ });
233
+ } else {
234
+ suggestions.push({
235
+ url: "https://web.archive.org/web/*/https://www.youtube.com/c/" + data.channel_custom,
236
+ text: "Archive.org - https://www.youtube.com/c/" + data.channel_custom
237
+ });
238
+ }
239
+ suggestions.push({
240
+ url: "https://web.archive.org/web/*/https://www.youtube.com/" + data.channel_custom,
241
+ text: "Archive.org - https://www.youtube.com/" + data.channel_custom
242
+ });
243
+ suggestions.push({
244
+ url: "https://archive.org/search.php?query=" + data.channel_custom,
245
+ text: "Archive.org (search) - " + data.channel_custom
246
+ });
247
+ }
248
+ if (data.hasOwnProperty("channel_id")) {
249
+ suggestions.push({
250
+ url: "https://www.google.com/search?q=\"" + data.channel_id + "\"",
251
+ text: "Google - \"" + data.channel_id + "\""
252
+ });
253
+ suggestions.push({
254
+ url: "https://web.archive.org/web/*/https://www.youtube.com/channel/" + data.channel_id,
255
+ text: "Archive.org - https://www.youtube.com/channel/" + data.channel_id
256
+ });
257
+ suggestions.push({
258
+ url: "https://archive.org/search.php?query=subject%3A%22" + encode(data.channel_id) + "%22",
259
+ text: "Archive.org (search) - subject:\"" + data.channel_id + "\""
260
+ });
261
+ suggestions.push({
262
+ url: "https://archive.org/search.php?query=" + data.channel_id,
263
+ text: "Archive.org (search) - " + data.channel_id
264
+ });
265
+ suggestions.push({
266
+ url: "https://socialblade.com/youtube/channel/" + data.channel_id,
267
+ text: "Socialblade.com - " + data.channel_id
268
+ })
269
+ suggestions.push({
270
+ url: "https://filmot.com/channel/" + data.channel_id,
271
+ text: "Filmot.com - https://filmot.com/channel/" + data.channel_id
272
+ });
273
+ // suggestions.push({
274
+ // url: "https://www.youtuberecover.com/channel?id=" + data.channel_id,
275
+ // text: "YouTube Recover - " + data.channel_id
276
+ // });
277
+ suggestions.push({
278
+ url: "https://hobune.stream/channels/" + data.channel_id,
279
+ text: "Hobune Archive - " + data.channel_id
280
+ });
281
+ }
282
+
283
+ return suggestions;
284
+ }
285
+
286
+ function getDuration(a, b) {
287
+ if (a.isBefore(b)) {
288
+ return moment.duration(b.diff(a));
289
+ } else {
290
+ return moment.duration(a.diff(b));
291
+ }
292
+ }
293
+
294
+ function formatBCP47(translation) {
295
+ const findings = [];
296
+
297
+ if (translation.language) {
298
+ findings.push(translation.language.name);
299
+ }
300
+
301
+ if (translation.country) {
302
+ findings.push(translation.country.name);
303
+ }
304
+
305
+ return findings.join(" / ");
306
+ }
307
+
308
+ function processLocalizations(partDiv, partJson) {
309
+ const translations = [];
310
+
311
+ for (let code in partJson) {
312
+ translations.push("<li><span class='orange'>" + String(code).toUpperCase() + "</span> which is <span class='orange'>" + formatBCP47(bcp47.lookup(code)) + "</span></li>")
313
+ }
314
+
315
+ partDiv.append("<p class='mb-15'>Localizations for..." +
316
+ "<ul>" + translations.join("") + "</ul>" +
317
+ "</p>")
318
+ }
319
+
320
+ function normalize(a, b) {
321
+ let normalizedA = a, normalizedB = b;
322
+ let gcdValue = 0, gcdA = 0, gcdB = 0;
323
+
324
+ if (a > 0 && b > 0) {
325
+ function gcd(p, q) {
326
+ if (q === 0) {
327
+ return p;
328
+ }
329
+ return gcd(q, p % q);
330
+ }
331
+
332
+ gcdValue = gcd(a, b);
333
+ gcdA = gcdValue === 0 ? 0 : a / gcdValue;
334
+ gcdB = gcdValue === 0 ? 0 : b / gcdValue;
335
+ }
336
+
337
+ if (gcdValue !== 0) {
338
+ if (gcdA > gcdB) {
339
+ normalizedA = gcdA / gcdB;
340
+ normalizedB = 1;
341
+ } else {
342
+ normalizedA = 1;
343
+ normalizedB = gcdB / gcdA;
344
+ }
345
+ }
346
+
347
+ return {a: normalizedA, b: normalizedB}
348
+ }
349
+
350
+ const partMap = {
351
+ /**
352
+ * Can't access part(s): fileDetails, processingDetails, suggestions
353
+ * Useless part(s): player, id
354
+ * Every other part below:
355
+ */
356
+ video: {
357
+ snippet: {
358
+ title: "Snippet",
359
+ postProcess: function (partJson, fullJson) {
360
+ const partDiv = $("#video-section #snippet");
361
+
362
+ const thumbs = partJson.thumbnails;
363
+ const thumbUrl = (thumbs.maxres || thumbs.high || thumbs.medium || thumbs.default || {url: "https://placehold.it/480x360"}).url;
364
+
365
+ partDiv.append("<a target='_blank' href='https://youtu.be/" + fullJson.id + "'><img id='video-thumb' src='" + thumbUrl + "' class='mb-15'></a>");
366
+
367
+ const titleHtml =
368
+ "<p class='mb-15' style='font-size: 1.25em'>" + partJson.title + "</p>";
369
+ partDiv.append(titleHtml);
370
+
371
+ const authorHtml =
372
+ "<p class='mb-15'>Published by " +
373
+ "<a href='https://www.youtube.com/channel/" + partJson.channelId + "' target='_blank'>" +
374
+ partJson.channelTitle +
375
+ "</a>" +
376
+ "</p>";
377
+ partDiv.append(authorHtml);
378
+
379
+ const published = new Date(partJson.publishedAt);
380
+ const dateHtml =
381
+ "<p class='mb-15'>Published on " +
382
+ "<span class='orange'>" + published.toUTCString() + "</span>" +
383
+ " (" + moment(published).utc().fromNow() + ") " + getConvertTime(published) +
384
+ "</p>";
385
+ partDiv.append(dateHtml);
386
+
387
+ if (partJson.tags) {
388
+ const tagsHtml =
389
+ "<p class='mb-15'>Tag(s): " +
390
+ "<span class='tag'>" + partJson.tags.join("</span><span class='comma'>, </span><span class='tag'>") + "</span>" +
391
+ "</p>";
392
+ partDiv.append(tagsHtml);
393
+ } else {
394
+ partDiv.append("<p class='mb-15'>There were no tags.</p>")
395
+ }
396
+
397
+ if (partJson.categoryId) {
398
+ const html =
399
+ "<p class='mb-15'>Category id is " +
400
+ "<span class='orange'>" + partJson.categoryId + "</span> which means " +
401
+ "<span class='orange'>" + ytCategory.lookup(partJson.categoryId) + "</span>" +
402
+ "</p>";
403
+
404
+ partDiv.append(html);
405
+ }
406
+
407
+ if (partJson.defaultLanguage) {
408
+ const code = partJson.defaultLanguage.toUpperCase();
409
+ const translated = bcp47.lookup(code);
410
+
411
+ partDiv.append("<p class='mb-15'>Default language is <span class='orange'>" + code + "</span> which means <span class='orange'>" + formatBCP47(translated) + "</span></p>")
412
+ }
413
+
414
+ if (partJson.defaultAudioLanguage) {
415
+ const code = partJson.defaultAudioLanguage.toUpperCase();
416
+ const translated = bcp47.lookup(code);
417
+
418
+ partDiv.append("<p class='mb-15'>Audio language is <span class='orange'>" + code + "</span> which means <span class='orange'>" + formatBCP47(translated) + "</span></p>")
419
+ }
420
+
421
+ partDiv.append("<p class='mb-15'>The video id is <span class='orange'>" + fullJson.id + "</span></p>");
422
+
423
+ partDiv.append("<p class='mb-15'><a style='display:inline;vertical-align:middle' target='_blank' href='./bulk?submit=true&url=https://www.youtube.com/channel/" + partJson.channelId + "'>" +
424
+ "<img src='./img/metadata.png' style='margin-left:4px;width:20px;height:20px;;margin-right:5px;' alt='youtube metadata icon' >" +
425
+ "Inspect the metadata for the rest of this channel's videos" +
426
+ "</a></p>");
427
+ }
428
+ },
429
+ statistics: {
430
+ title: "Statistics",
431
+ postProcess: function (partJson, fullJson) {
432
+ const partDiv = $("#video-section #statistics");
433
+
434
+ if (partJson.hasOwnProperty("likeCount") && partJson.hasOwnProperty("dislikeCount")) {
435
+ const normalized = normalize(partJson.likeCount, partJson.dislikeCount);
436
+
437
+ const html =
438
+ "<p class='mb-15'>" +
439
+ "Normalized like ratio: " +
440
+ "<span style='color:green'>" + Math.trunc(normalized.a) + " like(s)</span> per " +
441
+ "<span style='color:red'>" + Math.trunc(normalized.b) + " dislike(s)</span>" +
442
+ "</p>";
443
+ partDiv.append(html);
444
+ } else if (!partJson.hasOwnProperty("likeCount")) {
445
+ partDiv.append("<p class='mb-15'>This video has <span class='orange'>likes disabled.</span></p>");
446
+ }
447
+
448
+ if (!partJson.hasOwnProperty("viewCount")) {
449
+ partDiv.append("<p class='mb-15'>This video has <span class='orange'>view counts disabled.</span></p>")
450
+ }
451
+
452
+ if (!partJson.hasOwnProperty("commentCount")) {
453
+ partDiv.append("<p class='mb-15'>This video has <span class='orange'>comments disabled.</span></p>")
454
+ }
455
+
456
+ if (!partJson.hasOwnProperty("dislikeCount")) {
457
+ partDiv.append(
458
+ "<p class='mb-15'>YouTube no longer provides the <span class='orange'>dislikeCount</span> since 2021-12-13 " +
459
+ "(<a href='https://developers.google.com/youtube/v3/revision_history#november-18,-2021' target='_blank'>see more here</a>). " +
460
+ "</p>");
461
+ partDiv.append("<p class='mb-15'>Want dislikes back? Check out the " +
462
+ "<a href='https://returnyoutubedislike.com/' target='_blank'>return-youtube-dislike</a> project!" +
463
+ "</p>");
464
+ // Potentially output RYD dislikes from their API.
465
+ // $.ajax({
466
+ // type: "GET",
467
+ // url: "https://returnyoutubedislikeapi.com/votes?videoId=" + fullJson.id
468
+ // }).then(function (res) {
469
+ // const dislikes = shared.idx(["dislikes"], res);
470
+ // if (dislikes) {
471
+ // partDiv.append("<p class='mb-15'>RYD Estimated Dislikes: <span class='orange'>" + dislikes + "</span></p>")
472
+ // }
473
+ // });
474
+ }
475
+ }
476
+ },
477
+ recordingDetails: {
478
+ title: "Geolocation",
479
+ postProcess: function (partJson, fullJson) {
480
+ const partDiv = $("#video-section #recordingDetails");
481
+
482
+ const location = partJson.location;
483
+ if (location && location.latitude && location.longitude) {
484
+ const latlng = location.latitude + "," + location.longitude;
485
+ const staticMap = "https://maps.googleapis.com/maps/api/staticmap?center=" + latlng + "&zoom=14&size=1000x300&key=AIzaSyAa-o55aIMt4YC0mhPyp8WfGql5DVg_fp4&markers=color:red|" + latlng;
486
+
487
+ const link = partJson.hasOwnProperty("locationDescription") ?
488
+ "https://maps.google.com/maps/search/" + encodeURI(partJson.locationDescription).replace(/'/g, "%27") + "/@" + latlng + ",14z" :
489
+ "https://maps.google.com/maps?q=loc:" + latlng;
490
+
491
+ const html =
492
+ "<p class='mb-15'><a href='" + link + "' target='_blank'>" +
493
+ "<img src='" + staticMap + "' alt='Google Maps Static Map'>" +
494
+ "<p>Click to open in Google Maps</p>" +
495
+ "</a></p>";
496
+
497
+ partDiv.append(html);
498
+ }
499
+
500
+ if (partJson.recordingDate && fullJson.snippet) {
501
+ const recordDate = moment(partJson.recordingDate).utc();
502
+
503
+ const dateHtml =
504
+ "<p class='mt-15 mb-15'>Recorded on " +
505
+ "<span class='orange'>" + recordDate.format("ddd, DD MMM YYYY") + "</span>" +
506
+ " (" + recordDate.fromNow() + "). YouTube Studio only allows creators to pick the date so there is no time on this timestamp." +
507
+ "</p>";
508
+ partDiv.append(dateHtml);
509
+
510
+ const published = moment(fullJson.snippet.publishedAt).utc();
511
+ const format = shared.formatDuration(getDuration(recordDate, published), false, true);
512
+ if (format === "0s") {
513
+ partDiv.append("<p class='mb-15'>The video was recorded <span class='orange'>same day</span> as the publish date.</p>")
514
+ } else if (published.isAfter(recordDate)) {
515
+ partDiv.append("<p class='mb-15'>The video was recorded <span class='orange'>" + format + "</span> before the publish date.</p>")
516
+ } else {
517
+ partDiv.append("<p class='mb-15'>The video was recorded <span class='orange'>" + format + "</span> after the publish date.</p>");
518
+ }
519
+ }
520
+ }
521
+ },
522
+ status: {
523
+ title: "Status",
524
+ postProcess: function (partJson) {
525
+ const partDiv = $("#video-section #status");
526
+
527
+ if (partJson.hasOwnProperty("privacyStatus")) {
528
+ if (partJson.privacyStatus !== "public") {
529
+ partDiv.append("<p class='mb-15'>This video has its privacy set to <span class='orange'>" + partJson.privacyStatus + "</span></p>")
530
+ }
531
+ }
532
+ if (partJson.hasOwnProperty("license")) {
533
+ if (partJson.license !== "youtube") {
534
+ partDiv.append("<p class='mb-15'>This video has its license set to <span class='orange'>" + partJson.license + "</span></p>")
535
+ }
536
+ }
537
+ if (partJson.hasOwnProperty("embeddable")) {
538
+ if (partJson.embeddable) {
539
+ partDiv.append("<p class='mb-15'>This video may be embedded on other websites</p>")
540
+ } else {
541
+ partDiv.append("<p class='mb-15'>This video may not be embedded on other websites</p>")
542
+ }
543
+ }
544
+ if (partJson.hasOwnProperty("madeForKids")) {
545
+ if (partJson.madeForKids) {
546
+ partDiv.append("<p class='mb-15'>This video is designated as <span class='orange'>child-directed</span></p>")
547
+ } else {
548
+ partDiv.append("<p class='mb-15'>This video is not child-directed</p>")
549
+ }
550
+ }
551
+ if (partJson.hasOwnProperty("selfDeclaredMadeForKids")) {
552
+ if (partJson.selfDeclaredMadeForKids) {
553
+ partDiv.append("<p class='mb-15'>The video owner designated this video as <span class='orange'>child-directed</span></p>")
554
+ } else {
555
+ partDiv.append("<p class='mb-15'>The video owner designated this video as not child-directed.</p>")
556
+ }
557
+ }
558
+ }
559
+ },
560
+ liveStreamingDetails: {
561
+ title: "Livestream Details",
562
+ postProcess: function (partJson) {
563
+ const partDiv = $("#video-section #liveStreamingDetails");
564
+
565
+ if (partJson.hasOwnProperty("actualStartTime")) {
566
+ const start = new Date(partJson.actualStartTime);
567
+ const dateHtml =
568
+ "<p class='mb-15'>The stream started on " +
569
+ "<span class='orange'>" + start.toUTCString() + "</span>" +
570
+ " (" + moment(start).utc().fromNow() + ") " + getConvertTime(start) +
571
+ "</p>";
572
+ partDiv.append(dateHtml);
573
+ }
574
+ if (partJson.hasOwnProperty("actualEndTime")) {
575
+ const end = new Date(partJson.actualEndTime);
576
+ const dateHtml =
577
+ "<p class='mb-15'>The stream ended on " +
578
+ "<span class='orange'>" + end.toUTCString() + "</span>" +
579
+ " (" + moment(end).utc().fromNow() + ") " + getConvertTime(end) +
580
+ "</p>";
581
+ partDiv.append(dateHtml);
582
+ }
583
+
584
+ const now = moment(new Date()).utc();
585
+ if (partJson.hasOwnProperty("scheduledStartTime") && partJson.hasOwnProperty("scheduledEndTime")) {
586
+ const start = moment(partJson.scheduledStartTime).utc();
587
+ const end = moment(partJson.scheduledEndTime).utc();
588
+ const format = shared.formatDuration(getDuration(start, end));
589
+
590
+ partDiv.append("<p class='mb-15'>The stream was scheduled to run for <span class='orange'>" + format + "</span></p>");
591
+ }
592
+ if (partJson.hasOwnProperty("scheduledStartTime") && !partJson.hasOwnProperty("actualStartTime")) {
593
+ // Stream hasn't started
594
+ const start = moment(partJson.scheduledStartTime).utc();
595
+ const format = shared.formatDuration(getDuration(start, now));
596
+
597
+ if (start.isAfter(now)) {
598
+ partDiv.append("<p class='mb-15'>The stream hasn't started yet. It will start in <span class='orange'>" + format + "</span></p>");
599
+ } else {
600
+ partDiv.append("<p class='mb-15'>The stream hasn't started yet. It was supposed to start <span class='orange'>" + format + "</span> ago</p>");
601
+ }
602
+ }
603
+ if (partJson.hasOwnProperty("actualStartTime") && partJson.hasOwnProperty("scheduledStartTime")) {
604
+ // Stream started. Time between schedule date and actual start?
605
+ const start = moment(partJson.actualStartTime).utc();
606
+ const scheduled = moment(partJson.scheduledStartTime).utc();
607
+ const format = shared.formatDuration(getDuration(start, scheduled));
608
+ if (start.isAfter(scheduled)) {
609
+ partDiv.append("<p class='mb-15'>The stream was <span class='orange'>" + format + "</span> late to start</p>")
610
+ } else {
611
+ partDiv.append("<p class='mb-15'>The stream was <span class='orange'>" + format + "</span> early to start</p>");
612
+ }
613
+ }
614
+ if (partJson.hasOwnProperty("actualStartTime") && !partJson.hasOwnProperty("actualEndTime")) {
615
+ // Stream started but still going. Time between start and now?
616
+ const start = moment(partJson.actualStartTime).utc();
617
+ const format = shared.formatDuration(getDuration(start, now));
618
+
619
+ partDiv.append("<p class='mb-15'>The stream is still going. It has been running for <span class='orange'>" + format + "</span></p>");
620
+ }
621
+ if (partJson.hasOwnProperty("actualStartTime") && partJson.hasOwnProperty("actualEndTime")) {
622
+ // Stream done. Time between start and end?
623
+ const start = moment(partJson.actualStartTime).utc();
624
+ const end = moment(partJson.actualEndTime).utc();
625
+ const format = shared.formatDuration(getDuration(start, end));
626
+
627
+ partDiv.append("<p class='mb-15'>The stream is over. It's length was <span class='orange'>" + format + "</span></p>");
628
+ }
629
+ }
630
+ },
631
+ localizations: {
632
+ title: "Localizations",
633
+ postProcess: function (partJson) {
634
+ const partDiv = $("#video-section #localizations");
635
+
636
+ processLocalizations(partDiv, partJson);
637
+ }
638
+ },
639
+ contentDetails: {
640
+ title: "Content Details",
641
+ postProcess: function (partJson) {
642
+ const partDiv = $("#video-section #contentDetails");
643
+
644
+ const duration = moment.duration(partJson.duration);
645
+ const format = shared.formatDuration(duration);
646
+
647
+ if (format === "0s") {
648
+ partDiv.append("<p class='mb-15'>A video can't be 0 seconds. This must be a livestream.</p>");
649
+ } else {
650
+ partDiv.append("<p class='mb-15'>The video length was <span style='color:orange'>" + format + "</span></p>");
651
+ }
652
+
653
+ if (partJson.hasOwnProperty('regionRestriction')) {
654
+ const restriction = partJson.regionRestriction;
655
+
656
+ const totalIsoCodes = iso3166.codes.length;
657
+ // Should have only one or the other, never both
658
+ let message;
659
+ if (restriction.hasOwnProperty('allowed')) {
660
+ partDiv.append("<p class='mb-15'>This video is <span class='orange'>region-restriction</span> <span class='green'>allowed</span>.");
661
+ message = "These <span class='orange'>" + restriction.allowed.length + " / " + totalIsoCodes + "</span> region(s) are <span class='green'>allowed</span> to watch the video.</p>";
662
+ restriction.allowed.sort();
663
+ } else if (restriction.hasOwnProperty('blocked')) {
664
+ partDiv.append("<p class='mb-15'>This video is <span class='orange'>region-restriction</span> <span class='red'>blocked</span>.");
665
+ message = "These <span class='orange'>" + restriction.blocked.length + " / " + totalIsoCodes + "</span> region(s) are <span class='red'>not allowed</span> to watch the video.</p>";
666
+ restriction.blocked.sort();
667
+ }
668
+
669
+ const inList = restriction.allowed || restriction.blocked;
670
+ const translations = [];
671
+ inList.forEach(function (code) {
672
+ const result = iso3166.lookup(code);
673
+ const name = (result ? result.name : 'ISO-3166 Could not translate')
674
+
675
+ translations.push("<li><span class='orange'>" + String(code).toUpperCase() + "</span> which is <span class='orange'>" + name + "</span></li>")
676
+ });
677
+ partDiv.append(
678
+ "<p class='mb-15'><a data-bs-toggle='collapse' href='#restriction-list-1'>" + message + "</a></p>" +
679
+ "<div id='restriction-list-1' class='collapse'>" +
680
+ "<ul>" + translations.join("") + "</ul>" +
681
+ "</div>");
682
+
683
+ const notInList = [];
684
+ iso3166.codes.forEach(function (code) {
685
+ if (inList.indexOf(code.alpha2) === -1) {
686
+ notInList.push(code.alpha2);
687
+ }
688
+ });
689
+ let message2;
690
+ if (restriction.hasOwnProperty('allowed')) {
691
+ message2 = "These <span class='orange'>" + notInList.length + " / " + totalIsoCodes + "</span> region(s) are <span class='red'>not allowed</span> to watch the video.";
692
+ restriction.allowed.sort();
693
+ } else if (restriction.hasOwnProperty('blocked')) {
694
+ message2 = "These <span class='orange'>" + notInList.length + " / " + totalIsoCodes + "</span> region(s) are <span class='green'>allowed</span> to watch the video.";
695
+ restriction.blocked.sort();
696
+ }
697
+ notInList.sort();
698
+ const translations2 = [];
699
+ notInList.forEach(function (code) {
700
+ const result = iso3166.lookup(code);
701
+ const name = (result ? result.name : 'ISO-3166 Could not translate')
702
+
703
+ translations2.push("<li><span class='orange'>" + String(code).toUpperCase() + "</span> which is <span class='orange'>" + name + "</span></li>");
704
+ });
705
+ partDiv.append(
706
+ "<p class='mb-15'><a data-bs-toggle='collapse' href='#restriction-list-2'>" + message2 + "</a></p>" +
707
+ "<div id='restriction-list-2' class='collapse'>" +
708
+ "<ul>" + translations2.join("") + "</ul>" +
709
+ "</div>");
710
+ }
711
+
712
+ const contentRating = partJson.contentRating;
713
+ if (!$.isEmptyObject(contentRating)) {
714
+ const pairs = [];
715
+
716
+ Object.keys(contentRating).forEach(function (key) {
717
+ pairs.push(key + "/" + contentRating[key]);
718
+ });
719
+
720
+ partDiv.append("<p class='mb-15'>This video has a content rating of <span class='orange'>" + pairs.join(", ") + "</span></p>")
721
+ }
722
+ }
723
+ },
724
+ topicDetails: {
725
+ title: "Topic Details",
726
+ postProcess: function (partJson) {
727
+ const partDiv = $("#video-section #topicDetails");
728
+
729
+ const topics = [];
730
+ (partJson.topicCategories || []).forEach(function (categoryUrl) {
731
+ const text = categoryUrl.substr(categoryUrl.lastIndexOf('/') + 1).replace(/_/g, " ");
732
+
733
+ topics.push("<li><a target='_blank' href='" + categoryUrl + "'>" + text + "</a></li>");
734
+ });
735
+
736
+ partDiv.append("<p class='mb-15'><ul>" + topics.join("") + "</ul></p>");
737
+ }
738
+ }
739
+ },
740
+
741
+ /**
742
+ * Can't access part(s): auditDetails, contentOwnerDetails
743
+ * Useless part(s): id
744
+ * Every other part below:
745
+ */
746
+ channel: {
747
+ snippet: {
748
+ title: "Snippet",
749
+ postProcess: function (partJson, fullJson) {
750
+ const partDiv = $("#channel-section #snippet");
751
+
752
+ const thumbs = partJson.thumbnails;
753
+ const thumbUrl = (thumbs.maxres || thumbs.high || thumbs.medium || thumbs.default || {url: "https://placehold.it/240x240"}).url;
754
+
755
+ partDiv.append("<a target='_blank' href='https://www.youtube.com/channel/" + fullJson.id + "'><img id='channel-thumb' src='" + thumbUrl + "' class='mb-15 profile'></a>");
756
+ partDiv.append("<p class='mb-15' style='font-size: 1.25em'>" + partJson.title + "</p>");
757
+
758
+ const published = new Date(partJson.publishedAt);
759
+ const dateHtml =
760
+ "<p class='mb-15'>Channel created on " +
761
+ "<span class='orange'>" + published.toUTCString() + "</span>" +
762
+ " (" + moment(published).utc().fromNow() + ")" + getConvertTime(published) +
763
+ "</p>";
764
+ partDiv.append(dateHtml);
765
+
766
+ if (partJson.hasOwnProperty("country")) {
767
+ const countryCode = partJson.country;
768
+ const country = bcp47.lookup(countryCode).country;
769
+ const translated = country ? " which is <span class='orange'>" + country.name + "</span>" : "";
770
+ partDiv.append("<p class='mb-15'>The channel is associated with country code <span class='orange'>" + countryCode + "</span>" + translated + "</p>");
771
+ } else {
772
+ partDiv.append("<p class='mb-15'>The channel doesn't have an associated country.</p>");
773
+ }
774
+
775
+ if (partJson.hasOwnProperty("customUrl")) {
776
+ const urlPart = partJson.customUrl.startsWith('@') ? '' : 'c/';
777
+ const customUrl = "https://www.youtube.com/" + urlPart + partJson.customUrl;
778
+
779
+ partDiv.append("<p class='mb-15'>The channel has a custom url of value '<a target='_blank' href='" + customUrl + "'>" + partJson.customUrl + "</a>'</p>");
780
+ }
781
+
782
+ partDiv.append("<p class='mb-15'>The channel id is <span class='orange'>" + fullJson.id + "</span></p>");
783
+ }
784
+ },
785
+ statistics: {
786
+ title: "Statistics",
787
+ postProcess: function (partJson, fullJson) {
788
+ const partDiv = $("#channel-section #statistics");
789
+
790
+ const subLevels = {
791
+ graphite: {
792
+ min: 1,
793
+ max: 999,
794
+ qualCount: "1-1k",
795
+ learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/graphite/"
796
+ },
797
+ opal: {
798
+ min: 1000,
799
+ max: 9999,
800
+ qualCount: "1k-10k",
801
+ learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/opal/"
802
+ },
803
+ bronze: {
804
+ min: 10000,
805
+ max: 99999,
806
+ qualCount: "10k-100k",
807
+ learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/bronze/"
808
+ },
809
+ silver: {
810
+ min: 100000,
811
+ max: 999999,
812
+ qualCount: "100k-1m",
813
+ learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/silver/"
814
+ },
815
+ gold: {
816
+ min: 1000000,
817
+ max: 9999999,
818
+ qualCount: "1m-10m",
819
+ learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/silver/"
820
+ },
821
+ diamond: {
822
+ min: 10000000,
823
+ max: Number.MAX_SAFE_INTEGER,
824
+ qualCount: "10m+",
825
+ learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/silver/"
826
+ }
827
+ }
828
+
829
+ const subs = partJson.subscriberCount;
830
+ const learnMore = "Click <a target='_blank' href='https://www.youtube.com/intl/en-GB/creators/benefits/'>here</a> to learn more.";
831
+
832
+ for (let levelName in subLevels) {
833
+ const level = subLevels[levelName];
834
+ if (subs >= level.min && subs <= level.max) {
835
+ partDiv.append("<p class='mb-15'>This channel's subscriber count qualifies for benefit level <span class='orange'>" + levelName + " </span> (" + level.qualCount + "). " + learnMore + "</p>")
836
+ }
837
+ }
838
+
839
+ if (partJson.hiddenSubscriberCount) {
840
+ partDiv.append("<p class='mb-15'>This channel has their subscriber count <span class='orange'>hidden</span>.</p>");
841
+ } else if (subs <= 0) {
842
+ partDiv.append("<p class='mb-15'>This channel has no subscribers and does not qualify for any benefit level. " + learnMore + "</p>");
843
+ } else {
844
+ partDiv.append("<p class='mb-15'>Check out this channel on <a target='_blank' href='https://socialblade.com/youtube/channel/" + fullJson.id + "'>SocialBlade</a>.</p>");
845
+ }
846
+
847
+ if (partJson.videoCount > 0) {
848
+ partDiv.append("<p class='mb-15'><a style='display:inline;vertical-align:middle' target='_blank' href='./bulk?submit=true&url=https://www.youtube.com/channel/" + fullJson.id + "'>" +
849
+ "<img src='./img/metadata.png' style='margin-left:4px;width:20px;height:20px;;margin-right:5px;' alt='youtube metadata icon' >" +
850
+ "Inspect the metadata for all of this channel's videos" +
851
+ "</a></p>");
852
+ } else {
853
+ partDiv.append("<p class='mb-15'>This channel has no public videos.</p>");
854
+ }
855
+ }
856
+ },
857
+ brandingSettings: {
858
+ title: "Branding Settings",
859
+ postProcess: function (partJson) {
860
+ const partDiv = $("#channel-section #brandingSettings");
861
+
862
+ const bannerImage = shared.idx(["image", "bannerExternalUrl"], partJson);
863
+ if (bannerImage) {
864
+ partDiv.append("<img id='channel-banner' src='" + bannerImage + "' class='mb-15'>");
865
+ }
866
+
867
+ if (partJson.channel.hasOwnProperty("trackingAnalyticsAccountId")) {
868
+ partDiv.append("<p class='mb-15'>This channel is tracking and measuring traffic with Google Analytics <span class='orange'>" + partJson.channel.trackingAnalyticsAccountId + "</span></p>")
869
+ }
870
+
871
+ if (partJson.channel.hasOwnProperty("moderateComments")) {
872
+ if (partJson.channel.moderateComments) {
873
+ partDiv.append("<p class='mb-15'>Comments on the channel page are <span class='orange'>moderated</span> and require approval by the owner.</p>")
874
+ } else {
875
+ partDiv.append("<p class='mb-15'>Comments on the channel page are not moderated.</p>")
876
+ }
877
+ }
878
+
879
+ if (partJson.channel.keywords) {
880
+ const keywords = partJson.channel.keywords;
881
+ const parsed = [];
882
+
883
+ // Custom parser because regex for all languages is funky
884
+ // and why doesn't browser JS regex support \p{L} !?!?!
885
+ // Also, why didn't google make this an array like video tags?
886
+ let word = "";
887
+ let inQuotes = false;
888
+ for (let i = 0; i < keywords.length; i++) {
889
+ const char = keywords.charAt(i);
890
+
891
+ if (char === '"' && inQuotes === false) {
892
+ inQuotes = true;
893
+ } else if (char === '"' && inQuotes === true) {
894
+ inQuotes = false;
895
+ }
896
+
897
+ if (char !== '"') {
898
+ if (char === " " && inQuotes === false) {
899
+ parsed.push(word);
900
+ word = "";
901
+ } else if (char !== " " || inQuotes === true) {
902
+ word += char;
903
+ }
904
+ }
905
+ }
906
+ if (parsed.indexOf(word) === -1) {
907
+ parsed.push(word);
908
+ }
909
+
910
+ const keywordsHtml =
911
+ "<p class='mb-15'>Channel Keyword(s): " +
912
+ (parsed && parsed.length ?
913
+ "<span class='tag'>" +
914
+ parsed.join("</span><span class='comma'>, </span><span class='tag'>") +
915
+ "</span>" : "") +
916
+ "</p>";
917
+ partDiv.append(keywordsHtml);
918
+ } else {
919
+ partDiv.append("<p class='mb-15'>There were no keywords.</p>")
920
+ }
921
+ }
922
+ },
923
+ contentDetails: {
924
+ title: "Content Details",
925
+ postProcess: function (partJson) {
926
+ const partDiv = $("#channel-section #contentDetails");
927
+
928
+ const related = partJson.relatedPlaylists;
929
+ if (related) {
930
+ if (related.hasOwnProperty("uploads") && related.uploads) {
931
+ partDiv.append("<p class='mb-15'><a target='_blank' href='https://www.youtube.com/playlist?list=" + related.uploads + "'>Uploads playlist</a></p>")
932
+ }
933
+ if (related.hasOwnProperty("favorites") && related.favorites) {
934
+ partDiv.append("<p class='mb-15'><a target='_blank' href='https://www.youtube.com/playlist?list=" + related.favorites + "'>Favorites playlist</a></p>")
935
+ }
936
+ if (related.hasOwnProperty("likes") && related.likes) {
937
+ partDiv.append("<p class='mb-15'><a target='_blank' href='https://www.youtube.com/playlist?list=" + related.likes + "'>Likes playlist</a></p>")
938
+ }
939
+ }
940
+ }
941
+ },
942
+ localizations: {
943
+ title: "Localizations",
944
+ postProcess: function (partJson) {
945
+ const partDiv = $("#channel-section #localizations");
946
+
947
+ processLocalizations(partDiv, partJson);
948
+ }
949
+ },
950
+ status: {
951
+ title: "Status",
952
+ postProcess: function (partJson) {
953
+ const partDiv = $("#channel-section #status");
954
+
955
+ if (partJson.hasOwnProperty("madeForKids")) {
956
+ if (partJson.madeForKids) {
957
+ partDiv.append("<p class='mb-15'>This channel is designated as <span class='orange'>child-directed</span></p>")
958
+ } else {
959
+ partDiv.append("<p class='mb-15'>This channel is not child-directed</p>")
960
+ }
961
+ }
962
+ if (partJson.hasOwnProperty("selfDeclaredMadeForKids")) {
963
+ if (partJson.selfDeclaredMadeForKids) {
964
+ partDiv.append("<p class='mb-15'>The channel owner designated this channel as <span class='orange'>child-directed</span></p>")
965
+ } else {
966
+ partDiv.append("<p class='mb-15'>The channel owner designated this channel as not child-directed.</p>")
967
+ }
968
+ }
969
+ }
970
+ },
971
+ topicDetails: {
972
+ title: "Topic Details",
973
+ postProcess: function (partJson) {
974
+ const partDiv = $("#channel-section #topicDetails");
975
+
976
+ const topics = [];
977
+ (partJson.topicCategories || []).forEach(function (categoryUrl) {
978
+ const text = categoryUrl.substr(categoryUrl.lastIndexOf('/') + 1).replace(/_/g, " ");
979
+
980
+ topics.push("<li><a target='_blank' href='" + categoryUrl + "'>" + text + "</a></li>");
981
+ });
982
+
983
+ partDiv.append("<p class='mb-15'><ul>" + topics.join("") + "</ul></p>");
984
+ }
985
+ }
986
+ },
987
+
988
+ /**
989
+ * Useless part(s): id, player
990
+ * Every other part below:
991
+ */
992
+ playlist: {
993
+ snippet: {
994
+ title: "Snippet",
995
+ postProcess: function (partJson, fullJson) {
996
+ const partDiv = $("#playlist-section #snippet");
997
+
998
+ const thumbs = partJson.thumbnails;
999
+ const thumbUrl = (thumbs.maxres || thumbs.high || thumbs.medium || thumbs.default || {url: "https://placehold.it/480x360"}).url;
1000
+
1001
+ partDiv.append("<a target='_blank' href='https://www.youtube.com/playlist?list=" + fullJson.id + "'><img id='playlist-thumb' src='" + thumbUrl + "' class='mb-15'></a>");
1002
+ partDiv.append("<p class='mb-15' style='font-size: 1.25em'>" + partJson.title + "</p>");
1003
+
1004
+ const authorHtml =
1005
+ "<p class='mb-15'>Published by " +
1006
+ "<a href='https://www.youtube.com/channel/" + partJson.channelId + "' target='_blank'>" +
1007
+ partJson.channelTitle +
1008
+ "</a>" +
1009
+ "</p>";
1010
+ partDiv.append(authorHtml);
1011
+
1012
+ const published = new Date(partJson.publishedAt);
1013
+ const dateHtml =
1014
+ "<p class='mb-15'>Playlist created on " +
1015
+ "<span class='orange'>" + published.toUTCString() + "</span>" +
1016
+ " (" + moment(published).utc().fromNow() + ")" + getConvertTime(published) +
1017
+ "</p>";
1018
+ partDiv.append(dateHtml);
1019
+
1020
+ partDiv.append("<p class='mb-15'>The playlist id is <span class='orange'>" + fullJson.id + "</span></p>");
1021
+
1022
+ partDiv.append("<p class='mb-15'><a style='display:inline;vertical-align:middle' target='_blank' href='./bulk?submit=true&url=https://www.youtube.com/channel/" + fullJson.id + "'>" +
1023
+ "<img src='./img/metadata.png' style='margin-left:4px;width:20px;height:20px;margin-right:5px;' alt='youtube metadata icon' >" +
1024
+ "Inspect the metadata for all of this playlist's videos" +
1025
+ "</a></p>");
1026
+ }
1027
+ },
1028
+ status: {
1029
+ title: "Status",
1030
+ postProcess: function (partJson) {
1031
+ const partDiv = $("#playlist-section #status");
1032
+ }
1033
+ },
1034
+ localizations: {
1035
+ title: "Localizations",
1036
+ postProcess: function (partJson) {
1037
+ const partDiv = $("#playlist-section #localizations");
1038
+
1039
+ processLocalizations(partDiv, partJson);
1040
+ }
1041
+ },
1042
+ contentDetails: {
1043
+ title: "Content Details",
1044
+ postProcess: function (partJson) {
1045
+ const partDiv = $("#playlist-section #contentDetails");
1046
+ }
1047
+ }
1048
+ }
1049
+ };
1050
+
1051
+ function errorState(message, funcAppend, errorJson) {
1052
+ $("#video,#playlist,#channel").hide();
1053
+
1054
+ let reasonAppend;
1055
+ const reason = shared.idx(["responseJSON", "error", "errors", 0, "reason"], errorJson);
1056
+ if (reason === "quotaExceeded") {
1057
+ $("#quota").show();
1058
+ reasonAppend = $("#quota-reason-append");
1059
+ reasonAppend.empty();
1060
+ } else {
1061
+ $("#unknown").show();
1062
+ $("#reason").html(message);
1063
+
1064
+ reasonAppend = $("#reason-append");
1065
+ reasonAppend.empty();
1066
+
1067
+ if (funcAppend) {
1068
+ funcAppend(reasonAppend);
1069
+ }
1070
+ }
1071
+ if (errorJson) {
1072
+ reasonAppend.append("<pre><code class='prettyprint language-json'></code></pre>");
1073
+
1074
+ const json = reasonAppend.find("code");
1075
+ json.text(JSON.stringify(errorJson, null, 4));
1076
+ hljs.highlightElement(json[0]);
1077
+ }
1078
+ }
1079
+
1080
+ function attemptLoadFilmot(parsedInput) {
1081
+ $("#filmot").hide();
1082
+
1083
+ if (parsedInput.type === "video_id") {
1084
+ // Note: Filmot has limited resources. Do not misuse, please contact about usage first.
1085
+ // https://filmot.com/contactus
1086
+ const hostname = window.location.hostname;
1087
+ if (hostname !== "localhost" && hostname !== "mattw.io") {
1088
+ console.log("do not call filmot from other instances")
1089
+ return;
1090
+ }
1091
+ if (!shared.isValidVideoId(parsedInput.value)) {
1092
+ console.log("do not call filmot for invalid ids")
1093
+ $("#reason-append").append("<p class='mb-15'>However, this is an <span class='orange'>invalid</span> video id and must follow pattern: <span class='orange'>[A-Za-z0-9_-]{10}[AEIMQUYcgkosw048]</span>.</p>");
1094
+ return;
1095
+ }
1096
+
1097
+ $.ajax({
1098
+ cache: false,
1099
+ data: {
1100
+ key: "md5paNgdbaeudounjp39",
1101
+ id: parsedInput.value,
1102
+ flags: 1 // Get channel and description too,
1103
+ },
1104
+ dataType: "json",
1105
+ type: "GET",
1106
+ timeout: 5000,
1107
+ url: "https://filmot.com/api/getvideos",
1108
+ }).done(function (res) {
1109
+ const filmotAppend = $("#filmot-append");
1110
+ filmotAppend.empty();
1111
+
1112
+ const video = shared.idx([0], res);
1113
+ if (!video) {
1114
+ filmotAppend.append("<p class='mb-15'>No archive about this video id.</p>");
1115
+ $("#filmot").show();
1116
+ return;
1117
+ }
1118
+
1119
+ exportData["filmot"] = video;
1120
+
1121
+ filmotAppend.append("<pre><code class='prettyprint language-json'></code></pre>");
1122
+ const json = filmotAppend.find("code");
1123
+ json.text(JSON.stringify(video, null, 4));
1124
+ hljs.highlightElement(json[0]);
1125
+
1126
+ $("#filmot").show();
1127
+
1128
+ const titleHtml = "<p class='mb-15' style='font-size: 1.25em'>" + video.title + "</p>";
1129
+ filmotAppend.append(titleHtml);
1130
+
1131
+ const authorHtml =
1132
+ "<p class='mb-15'>Published by " +
1133
+ "<a href='https://www.youtube.com/channel/" + video.channelid + "' target='_blank'>" +
1134
+ video.channelname +
1135
+ "</a>" +
1136
+ "</p>";
1137
+ filmotAppend.append(authorHtml);
1138
+
1139
+ const published = new Date(video.uploaddate);
1140
+ const dateHtml =
1141
+ "<p class='mb-15'>Published on " +
1142
+ "<span class='orange'>" + moment(published).format("ddd, DD MMM YYYY") + "</span>" +
1143
+ " (" + moment(published).utc().fromNow() + ")" +
1144
+ "</p>";
1145
+ filmotAppend.append(dateHtml);
1146
+
1147
+ const duration = moment.duration({"seconds": video.duration});
1148
+ const format = shared.formatDuration(duration);
1149
+ if (format === "0s") {
1150
+ filmotAppend.append("<p class='mb-15'>A video can't be 0 seconds. This must be a livestream.</p>");
1151
+ } else {
1152
+ filmotAppend.append("<p class='mb-15'>The video length was <span style='color:orange'>" + format + "</span></p>");
1153
+ }
1154
+
1155
+ filmotAppend.append(getSuggestedHtml(null, null, null, video));
1156
+ }).fail(function (err) {
1157
+ console.warn(err);
1158
+ })
1159
+ } else if (parsedInput.type === "channel_id") {
1160
+ if (!shared.isValidChannelId(parsedInput.value)) {
1161
+ $("#reason-append").append("<p class='mb-15'>However, this is an <span class='orange'>invalid</span> channel id and must follow pattern: <span class='orange'>[A-Za-z0-9_-]{21}[AQgw]</span>.</p>");
1162
+ }
1163
+ }
1164
+ }
1165
+
1166
+ function attemptWaybackCDX(parsedInput) {
1167
+ if (parsedInput.type === "video_id") {
1168
+ $("#wayback").show();
1169
+ $("#wayback-show-older,#wayback-copy").hide();
1170
+ $("#wayback-append").html("Checking... <span id='wayback-progress'></span>")
1171
+
1172
+ const results = []
1173
+ const promises = []
1174
+ const cdxUrls = []
1175
+
1176
+ // Video thumbs
1177
+ cdxUrls.push("https://web.archive.org/cdx/search/cdx?url=i.ytimg.com/vi/" + parsedInput.value + "*&collapse=digest&filter=statuscode:200&mimetype:image/jpeg&output=json")
1178
+ cdxUrls.push("https://web.archive.org/cdx/search/cdx?url=s.ytimg.com/vi/" + parsedInput.value + "*&collapse=digest&filter=statuscode:200&mimetype:image/jpeg&output=json")
1179
+ cdxUrls.push("https://web.archive.org/cdx/search/cdx?url=img.youtube.com/vi/" + parsedInput.value + "*&collapse=digest&filter=statuscode:200&mimetype:image/jpeg&output=json")
1180
+
1181
+ // Storyboard thumbs
1182
+ const sbSub = ["i", "i9", "i1", "i2", "i3", "i4", "i5", "i6", "i7", "i8"]
1183
+ for (let i in sbSub) {
1184
+ const subdomain = sbSub[i];
1185
+ const cdxUrl = "https://web.archive.org/cdx/search/cdx?url=" + subdomain + ".ytimg.com/sb/" + parsedInput.value + "*&collapse=digest&filter=statuscode:200&mimetype:image/jpeg&output=json"
1186
+
1187
+ cdxUrls.push(cdxUrl)
1188
+ }
1189
+
1190
+ let progress = 0
1191
+ let failed = 0
1192
+ $("#wayback-progress").html(`${progress+failed}/${cdxUrls.length} done; ${failed} failed`)
1193
+
1194
+ promises.push(new Promise(function (resolve) {
1195
+ function cdxCall(i) {
1196
+ if (i >= cdxUrls.length) {
1197
+ console.log("promise done")
1198
+ resolve()
1199
+ return
1200
+ }
1201
+ console.log("calling " + i)
1202
+
1203
+ const url = cdxUrls[i]
1204
+ if (!url) {
1205
+ resolve()
1206
+ return
1207
+ }
1208
+
1209
+ const promise = new Promise(function (resolve2) {
1210
+ $.ajax({
1211
+ url: "https://cors-proxy-mw324.herokuapp.com/" + url,
1212
+ }).done(function (res) {
1213
+ console.log(url)
1214
+ if (res) {
1215
+ for (let i = 0; i < res.length; i++) {
1216
+ results.push(res[i])
1217
+ }
1218
+ }
1219
+ progress += 1
1220
+ $("#wayback-progress").html(`${progress+failed}/${cdxUrls.length} done; ${failed} failed`)
1221
+ resolve2()
1222
+ }).fail(function (err) {
1223
+ console.error(url)
1224
+ failed += 1
1225
+ $("#wayback-progress").html(`${progress+failed}/${cdxUrls.length} done; ${failed} failed`)
1226
+ resolve2()
1227
+ })
1228
+ })
1229
+
1230
+ promises.push(promise)
1231
+ promise.then(function () {cdxCall(i + 1)})
1232
+ }
1233
+ cdxCall(0)
1234
+ }))
1235
+
1236
+ Promise.all(promises).then(function () {
1237
+ console.log("Done!")
1238
+ console.log(results)
1239
+
1240
+ const links = []
1241
+ for (let i in results) {
1242
+ const result = results[i];
1243
+ if (result[0] === "urlkey") {
1244
+ continue
1245
+ }
1246
+
1247
+ const url = new URL(result[2])
1248
+ const base = url.hostname + url.pathname
1249
+
1250
+ links.push([base, result[2], result[1], `https://web.archive.org/web/${result[1]}/${result[2]}`])
1251
+ }
1252
+
1253
+ if (links.length) {
1254
+ links.sort(function (a,b) {
1255
+ // url base
1256
+ if (a[0] < b[0]) return -1
1257
+ if (a[0] > b[0]) return 1
1258
+
1259
+ // timestamp
1260
+ return b[2] - a[2]
1261
+ })
1262
+
1263
+ const hideOlder = $("#checkHideOlder").is(":checked")
1264
+
1265
+ function formatTime(waybackTime) {
1266
+ return waybackTime.replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, "$1-$2-$3 $4:$5:$6")
1267
+ }
1268
+
1269
+ let currentBase = null;
1270
+ const linkHtml = []
1271
+ for (let i in links) {
1272
+ const link = links[i];
1273
+
1274
+ if (link[0] === currentBase) {
1275
+ linkHtml.push(`<li class="cdx-link wayback-cdx-older" ${hideOlder ? "style='display:none'" : ""}>
1276
+ <a target="_blank" href="${link[3]}">Archive.org - ${link[0]}</a> <small class="text-muted">${formatTime(link[2])}</small></li>`)
1277
+ continue
1278
+ }
1279
+
1280
+ currentBase = link[0]
1281
+ linkHtml.push(`<li class="cdx-link wayback-cdx-newest"><a target="_blank" href="${link[3]}">Archive.org - ${link[0]}</a> <small class="text-muted">${formatTime(link[2])}</small></li>`)
1282
+ }
1283
+ $("#wayback-append").html(`<ul>${linkHtml.join("")}</ul>`)
1284
+ $("#wayback-show-older,#wayback-copy").show();
1285
+ $("#wayback-show-older .older-count").text($(".wayback-cdx-older").length)
1286
+ } else {
1287
+ $("#wayback-append").text("No cdx records were found.")
1288
+ }
1289
+ })
1290
+ }
1291
+ }
1292
+
1293
+ function parseType(partMapType, sectionId, res, parsedInput) {
1294
+ if (res && res.items && res.items.length > 0) {
1295
+ const item = res.items[0];
1296
+
1297
+ exportData[partMapType] = res;
1298
+
1299
+ for (let part in partMap[partMapType]) {
1300
+ const section = $("#" + sectionId + " #" + part);
1301
+ const sectionHeader = $(section.find(".section-header"));
1302
+
1303
+ if (item.hasOwnProperty(part) && !$.isEmptyObject(item[part])) {
1304
+ sectionHeader.removeClass("unknown").addClass("good");
1305
+ sectionHeader.find("i").removeClass("bi-question-circle-fill").addClass("bi-check-circle-fill");
1306
+ } else {
1307
+ sectionHeader.removeClass("unknown").addClass("bad");
1308
+ sectionHeader.find("i").removeClass("bi-question-circle-fill").addClass("bi-dash-circle-fill");
1309
+ }
1310
+
1311
+ if (item.hasOwnProperty(part)) {
1312
+ if (part === 'localizations') {
1313
+ item[part] = shared.sortObject(item[part]);
1314
+ }
1315
+
1316
+ section.append("<pre><code class='prettyprint language-json'></code></pre>");
1317
+
1318
+ const json = section.find("code");
1319
+ json.text(JSON.stringify(item[part], null, 4));
1320
+ hljs.highlightElement(json[0]);
1321
+
1322
+ partMap[partMapType][part].postProcess(item[part], item);
1323
+ }
1324
+
1325
+ if (!item.hasOwnProperty(part) || $.isEmptyObject(item[part])) {
1326
+ section.append("<p class='mb-15 bad'>The " + partMapType + " does not have " + part + ".</p>");
1327
+ }
1328
+ }
1329
+ } else {
1330
+ errorState("Your input looked like a <span class='orange'>" + partMapType + "</span> but nothing came back. " +
1331
+ "It may have been <a target='_blank' href='https://github.com/mattwright324/youtube-metadata/wiki/Deleted-and-Private-Videos'>deleted or made private</a>.", function (append) {
1332
+ append.append("<p class='mb-15'>" +
1333
+ "You may find more details by trying..." +
1334
+ getSuggestedHtml(parsedInput) +
1335
+ "</p>");
1336
+ });
1337
+ attemptLoadFilmot(parsedInput);
1338
+ $("#wayback,#wayback-check").show()
1339
+ $("#wayback-append").html("")
1340
+ }
1341
+ }
1342
+
1343
+ async function parseVideo(res, input, skipGetChannel) {
1344
+ const channelId = shared.idx(["items", "0", "snippet", "channelId"], res)
1345
+ if (!skipGetChannel && channelId) {
1346
+ submit({
1347
+ type: 'channel_id',
1348
+ value: channelId,
1349
+ mayHideOthers: false
1350
+ });
1351
+ }
1352
+
1353
+ parseType("video", "video-section", res, input);
1354
+
1355
+ const id = input.value;
1356
+ const thumbsDiv = $("#thumbnails");
1357
+
1358
+ thumbsDiv.empty();
1359
+ const names = ["0", "hq1", "hq2", "hq3"]
1360
+ for (let i in names) {
1361
+ const name = names[i]
1362
+ const thumbUrl = "https://img.youtube.com/vi/" + id + "/" + name + ".jpg";
1363
+ const html =
1364
+ "<div class='column' style='margin-bottom: 1.5%!important;'>" +
1365
+ "<a href='https://lens.google.com/uploadbyurl?url=" + thumbUrl + "' target='_blank'>" +
1366
+ "<img id='video-thumb-" + name + "' src='" + thumbUrl + "' alt='Thumb " + name + "' style='max-width: 200px;'>" +
1367
+ "<p>Click to reverse image search</p>" +
1368
+ "</a>" +
1369
+ "</div>";
1370
+
1371
+ thumbsDiv.append(html);
1372
+ }
1373
+
1374
+ const videoMore = $("#video-more");
1375
+ videoMore.empty();
1376
+ videoMore.append(getSuggestedHtml(null, shared.idx(["items", 0], res), "video"));
1377
+ }
1378
+
1379
+ async function parsePlaylist(res, input, skipGetChannel) {
1380
+ const channelId = shared.idx(["items", "0", "snippet", "channelId"], res)
1381
+ if (!skipGetChannel && channelId) {
1382
+ submit({
1383
+ type: 'channel_id',
1384
+ value: channelId,
1385
+ mayHideOthers: false
1386
+ });
1387
+ }
1388
+
1389
+ parseType("playlist", "playlist-section", res, input);
1390
+
1391
+ const playlistMore = $("#playlist-more");
1392
+ playlistMore.empty();
1393
+ playlistMore.append(getSuggestedHtml(null, shared.idx(["items", 0], res), "playlist"));
1394
+ }
1395
+
1396
+ async function parseChannel(res, input) {
1397
+ parseType("channel", "channel-section", res, input);
1398
+
1399
+ const channelMore = $("#channel-more");
1400
+ channelMore.empty();
1401
+ channelMore.append(getSuggestedHtml(null, shared.idx(["items", 0], res), "channel"));
1402
+ }
1403
+
1404
+ /**
1405
+ * Attempt to resolve the channel handle URL via CORS workaround. Grab webpage content and extract url pattern.
1406
+ */
1407
+ async function resolveChannelHandleCORS(parsedInput, callbackResubmit) {
1408
+ console.log('Attempting to resolve custom channel via CORS')
1409
+
1410
+ $.ajax({
1411
+ url: "https://cors-proxy-mw324.herokuapp.com/https://www.youtube.com/@" + parsedInput.value,
1412
+ dataType: 'html'
1413
+ }).then(function (res) {
1414
+ const pageHtml = $("<div>").html(res);
1415
+ const channelId = pageHtml.find("meta[itemprop='channelId']").attr('content');
1416
+ const ogUrl = pageHtml.find("meta[property='og:url']").attr('content');
1417
+ const canonical = pageHtml.find("link[rel='canonical']").attr('href');
1418
+
1419
+ console.log('Retrieved [channelId=%s, ogUrl=%s, canonical=%s]', channelId, ogUrl, canonical);
1420
+
1421
+ const newParsed = shared.determineInput(channelId || ogUrl || canonical);
1422
+ if (newParsed.type !== "unknown") {
1423
+ callbackResubmit(newParsed);
1424
+ } else {
1425
+ errorState("Could not resolve Custom Channel URL", function (append) {
1426
+ append.append("<p class='mb-15'>" +
1427
+ "Custom channel URLs have no direct API method, an indirect resolving method was unable to find it. " +
1428
+ "</p>");
1429
+ append.append("<p class='mb-15'>" +
1430
+ "Verify that the custom URL actually exists, if it does than you may try manually resolving it. " +
1431
+ "</p>");
1432
+ append.append("<p class='mb-15'>" +
1433
+ "More detail about the issue and what you can do can be found here at " +
1434
+ "<a target='_blank' href='https://github.com/mattwright324/youtube-metadata/issues/1'>#1 - Channel custom url unsupported</a>." +
1435
+ "</p>");
1436
+ })
1437
+ }
1438
+ }).fail(function (err) {
1439
+ errorState("Could not resolve Custom Channel URL", function (append) {
1440
+ append.append("<p class='mb-15'>" +
1441
+ "Custom channel URLs have no direct API method, an indirect resolving method was unable to find it. " +
1442
+ "</p>");
1443
+ append.append("<p class='mb-15'>" +
1444
+ "Verify that the custom URL actually exists, if it does than you may try manually resolving it. " +
1445
+ "</p>");
1446
+ append.append("<p class='mb-15'>" +
1447
+ "More detail about the issue and what you can do can be found here at " +
1448
+ "<a target='_blank' href='https://github.com/mattwright324/youtube-metadata/issues/1'>#1 - Channel custom url unsupported</a>." +
1449
+ "</p>");
1450
+ })
1451
+ });
1452
+ }
1453
+
1454
+ async function submit(parsedInput) {
1455
+ console.log(parsedInput);
1456
+
1457
+ if (parsedInput.original) {
1458
+ controls.inputValue.val(parsedInput.original);
1459
+
1460
+ const baseUrl = location.origin + location.pathname;
1461
+ if (parsedInput.type === "video_id" || parsedInput.type === "playlist_id" || parsedInput.type === "channel_id") {
1462
+ controls.shareLink.val(baseUrl + "?url=" + encodeURIComponent(parsedInput.original) + "&submit=true");
1463
+ } else {
1464
+ controls.shareLink.val(baseUrl + "?url=" + encodeURIComponent(parsedInput.original) + "&submit=true");
1465
+ }
1466
+
1467
+ controls.shareLink.attr("disabled", false);
1468
+ }
1469
+
1470
+ if (parsedInput.type === 'unknown') {
1471
+ errorState("Your link did not follow an accepted format.");
1472
+ } else if (parsedInput.type === 'channel_handle' || parsedInput.type === 'channel_custom') {
1473
+ resolveChannelHandleCORS(parsedInput, submit);
1474
+ } else if (parsedInput.type === 'video_id') {
1475
+ console.log('grabbing video');
1476
+
1477
+ if (parsedInput.mayHideOthers) {
1478
+ $("#playlist").hide();
1479
+ }
1480
+
1481
+ youtube.ajax('videos', {
1482
+ part: Object.keys(partMap.video).join(','),
1483
+ id: parsedInput.value
1484
+ }).done(function (res) {
1485
+ console.log(res);
1486
+
1487
+ parseVideo(res, parsedInput);
1488
+ }).fail(function (err) {
1489
+ console.log(err);
1490
+
1491
+ errorState("There was a problem querying for the video.", null, err);
1492
+ });
1493
+ } else if (parsedInput.type === 'channel_id') {
1494
+ console.log('grabbing channel id');
1495
+
1496
+ if (parsedInput.mayHideOthers) {
1497
+ $("#video,#playlist").hide();
1498
+ }
1499
+
1500
+ youtube.ajax('channels', {
1501
+ part: "id," + Object.keys(partMap.channel).join(','),
1502
+ id: parsedInput.value
1503
+ }).done(function (res) {
1504
+ console.log(res);
1505
+
1506
+ parseChannel(res, parsedInput);
1507
+ }).fail(function (err) {
1508
+ console.error(err);
1509
+
1510
+ errorState("There was a problem querying for the channel.", null, err);
1511
+ });
1512
+ } else if (parsedInput.type === 'channel_user') {
1513
+ console.log('grabbing channel user');
1514
+
1515
+ if (parsedInput.mayHideOthers) {
1516
+ $("#video,#playlist").hide();
1517
+ }
1518
+
1519
+ youtube.ajax('channels', {
1520
+ part: Object.keys(partMap.channel).join(','),
1521
+ forUsername: parsedInput.value
1522
+ }).done(function (res) {
1523
+ console.log(res);
1524
+
1525
+ parseChannel(res, parsedInput);
1526
+ }).fail(function (err) {
1527
+ console.error(err);
1528
+
1529
+ errorState("There was a problem querying for the channel.", null, err);
1530
+ });
1531
+ } else if (parsedInput.type === 'playlist_id') {
1532
+ console.log('grabbing playlist');
1533
+
1534
+ if (parsedInput.mayHideOthers) {
1535
+ $("#video").hide();
1536
+ }
1537
+
1538
+ youtube.ajax('playlists', {
1539
+ part: Object.keys(partMap.playlist).join(','),
1540
+ id: parsedInput.value
1541
+ }).done(function (res) {
1542
+ console.log(res);
1543
+
1544
+ parsePlaylist(res, parsedInput);
1545
+ }).fail(function (err) {
1546
+ console.error(err);
1547
+
1548
+ errorState("There was a problem querying for the playlist.", null, err);
1549
+ });
1550
+ }
1551
+ }
1552
+
1553
+ const internal = {
1554
+ init: function () {
1555
+ elements.hljsTheme = $("#highlightjs-theme");
1556
+ controls.darkMode = $("#darkMode");
1557
+ controls.inputValue = $("#value");
1558
+ controls.btnSubmit = $("#submit");
1559
+ controls.shareLink = $("#shareLink");
1560
+
1561
+ new ClipboardJS(".clipboard");
1562
+
1563
+ controls.btnExport = $("#export");
1564
+ controls.btnImport = $("#import");
1565
+ controls.importFileChooser = $("#importFileChooser");
1566
+
1567
+ elements.videoSection = $("#video-section");
1568
+ elements.channelSection = $("#channel-section");
1569
+ elements.playlistSection = $("#playlist-section");
1570
+
1571
+ const randomVideoId = shared.randomFromList(EXAMPLE_VIDEOS);
1572
+ const exampleLink = "https://youtu.be/" + randomVideoId;
1573
+ controls.inputValue.val(exampleLink);
1574
+
1575
+ internal.buildPage(true);
1576
+ },
1577
+ buildPage: function (doSetup) {
1578
+ $(".part-section").remove();
1579
+ $("#thumbnails").empty();
1580
+
1581
+ for (let part in partMap.video) {
1582
+ const partData = partMap.video[part];
1583
+ const html =
1584
+ "<div id='" + part + "' class='part-section'>" +
1585
+ "<div class='section-header unknown'><i class='bi bi-question-circle-fill'></i><span>" + partData.title + "</span></div>" +
1586
+ "</div>";
1587
+ elements.videoSection.append(html);
1588
+ }
1589
+
1590
+ for (let part in partMap.channel) {
1591
+ const partData = partMap.channel[part];
1592
+ const html =
1593
+ "<div id='" + part + "' class='part-section'>" +
1594
+ "<div class='section-header unknown'><i class='bi bi-question-circle-fill'></i><span>" + partData.title + "</span></div>" +
1595
+ "</div>";
1596
+ elements.channelSection.append(html);
1597
+ }
1598
+
1599
+ for (let part in partMap.playlist) {
1600
+ const partData = partMap.playlist[part];
1601
+ const html =
1602
+ "<div id='" + part + "' class='part-section'>" +
1603
+ "<div class='section-header unknown'><i class='bi bi-question-circle-fill'></i></i><span>" + partData.title + "</span></div>" +
1604
+ "</div>";
1605
+ elements.playlistSection.append(html);
1606
+ }
1607
+
1608
+ if (doSetup) {
1609
+ internal.setupControls();
1610
+ }
1611
+ },
1612
+ setupControls: function () {
1613
+ function checkTheme() {
1614
+ if (DarkMode.getColorScheme() === "dark") {
1615
+ elements.hljsTheme.attr("href", "//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.3.1/build/styles/stackoverflow-dark.min.css");
1616
+ } else {
1617
+ elements.hljsTheme.attr("href", "//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.3.1/build/styles/stackoverflow-light.min.css")
1618
+ }
1619
+ }
1620
+
1621
+ controls.darkMode.change(function () {
1622
+ checkTheme();
1623
+ });
1624
+ checkTheme();
1625
+
1626
+ controls.inputValue.on('keypress', function (e) {
1627
+ if (e.originalEvent.code === "Enter") {
1628
+ controls.btnSubmit.click();
1629
+ }
1630
+ });
1631
+
1632
+ countdownCheck(delaySubmitKey, controls.btnSubmit, delay5SecMs, "submit");
1633
+
1634
+ controls.btnSubmit.on('click', function () {
1635
+ if (!can.submit) {
1636
+ return;
1637
+ }
1638
+ localStorage.setItem(delaySubmitKey, new Date());
1639
+ countdownCheck(delaySubmitKey, controls.btnSubmit, delay5SecMs, "submit");
1640
+
1641
+ exportData = {};
1642
+
1643
+ const value = controls.inputValue.val();
1644
+
1645
+ const baseUrl = location.origin + location.pathname;
1646
+ controls.shareLink.val(baseUrl + "?url=" + encodeURIComponent(value) + "&submit=true");
1647
+ controls.shareLink.attr("disabled", false);
1648
+
1649
+ const parsed = shared.determineInput(value);
1650
+ window["parsed"] = parsed
1651
+
1652
+ $("#video,#playlist,#channel").show();
1653
+ $("#unknown,#quota,#filmot,#wayback").hide();
1654
+ internal.buildPage(false);
1655
+ submit(parsed);
1656
+ });
1657
+
1658
+ $("#wayback-check").click(function () {
1659
+ if ($("#wayback").is(":visible")) {
1660
+ $("#wayback-check").hide()
1661
+ attemptWaybackCDX(window.parsed);
1662
+ }
1663
+ })
1664
+
1665
+ $("#checkHideOlder").change(function () {
1666
+ if ($("#checkHideOlder").is(":checked")) {
1667
+ $(".wayback-cdx-older").hide()
1668
+ } else {
1669
+ $(".wayback-cdx-older").show()
1670
+ }
1671
+ })
1672
+
1673
+ new ClipboardJS("#wayback-copy", {
1674
+ text: function (trigger) {
1675
+ const links = []
1676
+ $(".cdx-link:visible").each(function (i, el) {
1677
+ const link = $(el).find("a").attr("href")
1678
+ if (link) {
1679
+ links.push(link)
1680
+ }
1681
+ })
1682
+ return links.join("\n")
1683
+ }
1684
+ })
1685
+
1686
+ function getImageBinaryCorsProxy(fileName, imageUrl, zip) {
1687
+ return new Promise(function (resolve) {
1688
+ // CORS proxy workaround for downloading YouTube thumbnails in client-side app
1689
+ // https://github.com/Rob--W/cors-anywhere/issues/301#issuecomment-962623118
1690
+ console.log('Attempting to download image over CORS proxy: ' + imageUrl);
1691
+ const start = new Date();
1692
+ JSZipUtils.getBinaryContent("https://cors-proxy-mw324.herokuapp.com/" + imageUrl, function (err, data) {
1693
+ const ms = new Date() - start;
1694
+
1695
+ if (err) {
1696
+ console.log('Failed ' + fileName + " (" + ms + "ms)");
1697
+ console.warn("Could not get image: " + imageUrl)
1698
+ console.warn(err);
1699
+ } else {
1700
+ console.log('Retrieved ' + fileName + " (" + ms + "ms)");
1701
+ console.log("Creating " + fileName + "...");
1702
+ zip.file(fileName, data, {binary: true});
1703
+ }
1704
+
1705
+ resolve();
1706
+ });
1707
+ });
1708
+ }
1709
+
1710
+ controls.btnExport.on('click', async function () {
1711
+ controls.btnExport.addClass("loading").addClass("disabled");
1712
+
1713
+ const zip = new JSZip();
1714
+ console.log("Creating about.txt...")
1715
+ zip.file("about.txt",
1716
+ "Downloaded by YouTube Metadata " + new Date().toLocaleString() + "\n\n" +
1717
+ "URL: " + window.location + "\n\n" +
1718
+ "Input: " + controls.inputValue.val()
1719
+ );
1720
+
1721
+ const thumbLinks = {};
1722
+ if (exportData.hasOwnProperty("video")) {
1723
+ console.log("Creating video.json...");
1724
+ zip.file("video.json", JSON.stringify(exportData.video, null, 4));
1725
+
1726
+ const names = ["0", "hq1", "hq2", "hq3"]
1727
+ for (let i in names) {
1728
+ const name = names[i];
1729
+
1730
+ thumbLinks[`video-thumb-${name}.png`] = document.getElementById(`video-thumb-${name}`).src;
1731
+ }
1732
+ }
1733
+
1734
+ if (exportData.hasOwnProperty("playlist")) {
1735
+ console.log("Creating playlist.json...");
1736
+ zip.file("playlist.json", JSON.stringify(exportData.playlist, null, 4));
1737
+
1738
+ thumbLinks["playlist-thumb.png"] = document.getElementById('playlist-thumb').src;
1739
+ }
1740
+
1741
+ if (exportData.hasOwnProperty("channel")) {
1742
+ console.log("Creating channel.json...");
1743
+ zip.file("channel.json", JSON.stringify(exportData.channel, null, 4));
1744
+
1745
+ thumbLinks["channel-thumb.png"] = document.getElementById('channel-thumb').src;
1746
+ if (document.getElementById('channel-banner')) {
1747
+ thumbLinks["channel-banner.png"] = document.getElementById('channel-banner').src;
1748
+ }
1749
+ }
1750
+
1751
+ if (exportData.hasOwnProperty("filmot")) {
1752
+ console.log("Creating filmot.json...");
1753
+ zip.file("filmot.json", JSON.stringify(exportData.filmot, null, 4));
1754
+ }
1755
+
1756
+ const optionalImages = [];
1757
+ for (let fileName in thumbLinks) {
1758
+ optionalImages.push(getImageBinaryCorsProxy(fileName, thumbLinks[fileName], zip));
1759
+ }
1760
+
1761
+ Promise.all(optionalImages).then(function () {
1762
+ let hint = '';
1763
+ if (exportData.hasOwnProperty("video")) {
1764
+ hint = " (video-" + shared.idx(["items", 0, "snippet", "title"], exportData.video).substr(0, 15) + ")";
1765
+ } else if (exportData.hasOwnProperty("playlist")) {
1766
+ hint = " (playlist-" + shared.idx(["items", 0, "snippet", "title"], exportData.playlist).substr(0, 15) + ")";
1767
+ } else if (exportData.hasOwnProperty("channel")) {
1768
+ hint = " (channel-" + shared.idx(["items", 0, "snippet", "title"], exportData.channel).substr(0, 15) + ")";
1769
+ } else if (exportData.hasOwnProperty("filmot")) {
1770
+ hint = " (filmot-" + exportData.filmot.title.substr(0, 15);
1771
+ }
1772
+
1773
+ const fileName = shared.safeFileName("metadata" + hint + ".zip");
1774
+ console.log("Saving as " + fileName);
1775
+ zip.generateAsync({
1776
+ type: "blob",
1777
+ compression: "DEFLATE",
1778
+ compressionOptions: {
1779
+ level: 9
1780
+ }
1781
+ }).then(function (content) {
1782
+ saveAs(content, fileName);
1783
+
1784
+ controls.btnExport.removeClass("loading").removeClass("disabled");
1785
+ });
1786
+ });
1787
+ });
1788
+
1789
+ // Drag & Drop listener
1790
+ document.addEventListener("dragover", function (event) {
1791
+ event.preventDefault();
1792
+ });
1793
+ document.documentElement.addEventListener('drop', async function (e) {
1794
+ e.stopPropagation();
1795
+ e.preventDefault();
1796
+
1797
+ let file = e.dataTransfer.files[0];
1798
+ console.log("Loading file");
1799
+ console.log(file);
1800
+
1801
+ importFile(file);
1802
+ });
1803
+
1804
+ controls.importFileChooser.on('change', function (event) {
1805
+ console.log(event);
1806
+
1807
+ let file = event.target.files[0];
1808
+
1809
+ if (file) {
1810
+ controls.inputValue.val(file.name);
1811
+ } else {
1812
+ return;
1813
+ }
1814
+
1815
+ importFile(file);
1816
+ });
1817
+
1818
+ function importFile(file) {
1819
+ console.log("Importing from file " + file.name);
1820
+
1821
+ controls.btnImport.addClass("loading").addClass("disabled");
1822
+
1823
+ $("#video,#playlist,#channel").show();
1824
+ $("#unknown,#quota,#filmot,#wayback").hide();
1825
+ internal.buildPage(false);
1826
+
1827
+ function loadFile(fileName, parseMethod, inputType) {
1828
+ return JSZip.loadAsync(file).then(function (content) {
1829
+ const file = content.file(fileName);
1830
+ return file ? file.async("string") : null;
1831
+ }).then(function (text) {
1832
+ if (!text) {
1833
+ $("#" + inputType).hide();
1834
+ return;
1835
+ }
1836
+ const content = JSON.parse(text);
1837
+ console.log(content);
1838
+ parseMethod(content, {
1839
+ type: inputType,
1840
+ value: shared.idx(["items", 0, "id"], content)
1841
+ }, true);
1842
+ });
1843
+ }
1844
+
1845
+ loadFile("video.json", parseVideo, "video").then(function () {
1846
+ loadFile("playlist.json", parsePlaylist, "playlist").then(function () {
1847
+ loadFile("channel.json", parseChannel, "channel").then(function () {
1848
+ controls.btnImport.removeClass("loading").removeClass("disabled");
1849
+ })
1850
+ });
1851
+ });
1852
+ }
1853
+
1854
+ const query = shared.parseQuery(window.location.search);
1855
+ console.log(query);
1856
+ const input = query.url || query.id;
1857
+ if (input) {
1858
+ controls.inputValue.val(decodeURIComponent(input));
1859
+ }
1860
+ if (query.hasOwnProperty("submit") && String(query.submit).toLowerCase() === String(true)) {
1861
+ setTimeout(function () {
1862
+ controls.btnSubmit.click();
1863
+ }, 500);
1864
+ }
1865
+ }
1866
+ };
1867
+ $(document).ready(internal.init);
1868
+ }());
js/yt-category-translator.js ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Implementation to quickly translate YouTube category codes.
3
+ *
4
+ * @link https://developers.google.com/youtube/v3/docs/videoCategories/list?apix_params=%7B%22part%22%3A%5B%22snippet%22%5D%2C%22regionCode%22%3A%22US%22%7D
5
+ */
6
+ const ytCategory = (function () {
7
+ 'use strict';
8
+
9
+ const categories = [
10
+ {
11
+ "kind": "youtube#videoCategory",
12
+ "etag": "grPOPYEUUZN3ltuDUGEWlrTR90U",
13
+ "id": "1",
14
+ "snippet": {
15
+ "title": "Film & Animation",
16
+ "assignable": true,
17
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
18
+ }
19
+ },
20
+ {
21
+ "kind": "youtube#videoCategory",
22
+ "etag": "Q0xgUf8BFM8rW3W0R9wNq809xyA",
23
+ "id": "2",
24
+ "snippet": {
25
+ "title": "Autos & Vehicles",
26
+ "assignable": true,
27
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
28
+ }
29
+ },
30
+ {
31
+ "kind": "youtube#videoCategory",
32
+ "etag": "qnpwjh5QlWM5hrnZCvHisquztC4",
33
+ "id": "10",
34
+ "snippet": {
35
+ "title": "Music",
36
+ "assignable": true,
37
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
38
+ }
39
+ },
40
+ {
41
+ "kind": "youtube#videoCategory",
42
+ "etag": "HyFIixS5BZaoBdkQdLzPdoXWipg",
43
+ "id": "15",
44
+ "snippet": {
45
+ "title": "Pets & Animals",
46
+ "assignable": true,
47
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
48
+ }
49
+ },
50
+ {
51
+ "kind": "youtube#videoCategory",
52
+ "etag": "PNU8SwXhjsF90fmkilVohofOi4I",
53
+ "id": "17",
54
+ "snippet": {
55
+ "title": "Sports",
56
+ "assignable": true,
57
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
58
+ }
59
+ },
60
+ {
61
+ "kind": "youtube#videoCategory",
62
+ "etag": "5kFljz9YJ4lEgSfVwHWi5kTAwAs",
63
+ "id": "18",
64
+ "snippet": {
65
+ "title": "Short Movies",
66
+ "assignable": false,
67
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
68
+ }
69
+ },
70
+ {
71
+ "kind": "youtube#videoCategory",
72
+ "etag": "ANnLQyzEA_9m3bMyJXMhKTCOiyg",
73
+ "id": "19",
74
+ "snippet": {
75
+ "title": "Travel & Events",
76
+ "assignable": true,
77
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
78
+ }
79
+ },
80
+ {
81
+ "kind": "youtube#videoCategory",
82
+ "etag": "0Hh6gbZ9zWjnV3sfdZjKB5LQr6E",
83
+ "id": "20",
84
+ "snippet": {
85
+ "title": "Gaming",
86
+ "assignable": true,
87
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
88
+ }
89
+ },
90
+ {
91
+ "kind": "youtube#videoCategory",
92
+ "etag": "q8Cp4pUfCD8Fuh8VJ_yl5cBCVNw",
93
+ "id": "21",
94
+ "snippet": {
95
+ "title": "Videoblogging",
96
+ "assignable": false,
97
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
98
+ }
99
+ },
100
+ {
101
+ "kind": "youtube#videoCategory",
102
+ "etag": "cHDaaqPDZsJT1FPr1-MwtyIhR28",
103
+ "id": "22",
104
+ "snippet": {
105
+ "title": "People & Blogs",
106
+ "assignable": true,
107
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
108
+ }
109
+ },
110
+ {
111
+ "kind": "youtube#videoCategory",
112
+ "etag": "3Uz364xBbKY50a2s0XQlv-gXJds",
113
+ "id": "23",
114
+ "snippet": {
115
+ "title": "Comedy",
116
+ "assignable": true,
117
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
118
+ }
119
+ },
120
+ {
121
+ "kind": "youtube#videoCategory",
122
+ "etag": "0srcLUqQzO7-NGLF7QnhdVzJQmY",
123
+ "id": "24",
124
+ "snippet": {
125
+ "title": "Entertainment",
126
+ "assignable": true,
127
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
128
+ }
129
+ },
130
+ {
131
+ "kind": "youtube#videoCategory",
132
+ "etag": "bQlQMjmYX7DyFkX4w3kT0osJyIc",
133
+ "id": "25",
134
+ "snippet": {
135
+ "title": "News & Politics",
136
+ "assignable": true,
137
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
138
+ }
139
+ },
140
+ {
141
+ "kind": "youtube#videoCategory",
142
+ "etag": "Y06N41HP_WlZmeREZvkGF0HW5pg",
143
+ "id": "26",
144
+ "snippet": {
145
+ "title": "Howto & Style",
146
+ "assignable": true,
147
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
148
+ }
149
+ },
150
+ {
151
+ "kind": "youtube#videoCategory",
152
+ "etag": "yBaNkLx4sX9NcDmFgAmxQcV4Y30",
153
+ "id": "27",
154
+ "snippet": {
155
+ "title": "Education",
156
+ "assignable": true,
157
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
158
+ }
159
+ },
160
+ {
161
+ "kind": "youtube#videoCategory",
162
+ "etag": "Mxy3A-SkmnR7MhJDZRS4DuAIbQA",
163
+ "id": "28",
164
+ "snippet": {
165
+ "title": "Science & Technology",
166
+ "assignable": true,
167
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
168
+ }
169
+ },
170
+ {
171
+ "kind": "youtube#videoCategory",
172
+ "etag": "p3lEirEJApyEkuWpaGEHoF-m-aA",
173
+ "id": "29",
174
+ "snippet": {
175
+ "title": "Nonprofits & Activism",
176
+ "assignable": true,
177
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
178
+ }
179
+ },
180
+ {
181
+ "kind": "youtube#videoCategory",
182
+ "etag": "4pIHL_AdN2kO7btAGAP1TvPucNk",
183
+ "id": "30",
184
+ "snippet": {
185
+ "title": "Movies",
186
+ "assignable": false,
187
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
188
+ }
189
+ },
190
+ {
191
+ "kind": "youtube#videoCategory",
192
+ "etag": "Iqol1myDwh2AuOnxjtn2AfYwJTU",
193
+ "id": "31",
194
+ "snippet": {
195
+ "title": "Anime/Animation",
196
+ "assignable": false,
197
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
198
+ }
199
+ },
200
+ {
201
+ "kind": "youtube#videoCategory",
202
+ "etag": "tzhBKCBcYWZLPai5INY4id91ss8",
203
+ "id": "32",
204
+ "snippet": {
205
+ "title": "Action/Adventure",
206
+ "assignable": false,
207
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
208
+ }
209
+ },
210
+ {
211
+ "kind": "youtube#videoCategory",
212
+ "etag": "ii8nBGYpKyl6FyzP3cmBCevdrbs",
213
+ "id": "33",
214
+ "snippet": {
215
+ "title": "Classics",
216
+ "assignable": false,
217
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
218
+ }
219
+ },
220
+ {
221
+ "kind": "youtube#videoCategory",
222
+ "etag": "Y0u9UAQCCGp60G11Arac5Mp46z4",
223
+ "id": "34",
224
+ "snippet": {
225
+ "title": "Comedy",
226
+ "assignable": false,
227
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
228
+ }
229
+ },
230
+ {
231
+ "kind": "youtube#videoCategory",
232
+ "etag": "_YDnyT205AMuX8etu8loOiQjbD4",
233
+ "id": "35",
234
+ "snippet": {
235
+ "title": "Documentary",
236
+ "assignable": false,
237
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
238
+ }
239
+ },
240
+ {
241
+ "kind": "youtube#videoCategory",
242
+ "etag": "eAl2b-uqIGRDgnlMa0EsGZjXmWg",
243
+ "id": "36",
244
+ "snippet": {
245
+ "title": "Drama",
246
+ "assignable": false,
247
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
248
+ }
249
+ },
250
+ {
251
+ "kind": "youtube#videoCategory",
252
+ "etag": "HDAW2HFOt3SqeDI00X-eL7OELfY",
253
+ "id": "37",
254
+ "snippet": {
255
+ "title": "Family",
256
+ "assignable": false,
257
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
258
+ }
259
+ },
260
+ {
261
+ "kind": "youtube#videoCategory",
262
+ "etag": "QHiWh3niw5hjDrim85M8IGF45eE",
263
+ "id": "38",
264
+ "snippet": {
265
+ "title": "Foreign",
266
+ "assignable": false,
267
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
268
+ }
269
+ },
270
+ {
271
+ "kind": "youtube#videoCategory",
272
+ "etag": "ztKcSS7GpH9uEyZk9nQCdNujvGg",
273
+ "id": "39",
274
+ "snippet": {
275
+ "title": "Horror",
276
+ "assignable": false,
277
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
278
+ }
279
+ },
280
+ {
281
+ "kind": "youtube#videoCategory",
282
+ "etag": "Ids1sm8QFeSo_cDlpcUNrnEBYWA",
283
+ "id": "40",
284
+ "snippet": {
285
+ "title": "Sci-Fi/Fantasy",
286
+ "assignable": false,
287
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
288
+ }
289
+ },
290
+ {
291
+ "kind": "youtube#videoCategory",
292
+ "etag": "qhfgS7MzzZHIy_UZ1dlawl1GbnY",
293
+ "id": "41",
294
+ "snippet": {
295
+ "title": "Thriller",
296
+ "assignable": false,
297
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
298
+ }
299
+ },
300
+ {
301
+ "kind": "youtube#videoCategory",
302
+ "etag": "TxVSfGoUyT7CJ7h7ebjg4vhIt6g",
303
+ "id": "42",
304
+ "snippet": {
305
+ "title": "Shorts",
306
+ "assignable": false,
307
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
308
+ }
309
+ },
310
+ {
311
+ "kind": "youtube#videoCategory",
312
+ "etag": "o9w6eNqzjHPnNbKDujnQd8pklXM",
313
+ "id": "43",
314
+ "snippet": {
315
+ "title": "Shows",
316
+ "assignable": false,
317
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
318
+ }
319
+ },
320
+ {
321
+ "kind": "youtube#videoCategory",
322
+ "etag": "mLdyKd0VgXKDI6GevTLBAcvRlIU",
323
+ "id": "44",
324
+ "snippet": {
325
+ "title": "Trailers",
326
+ "assignable": false,
327
+ "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
328
+ }
329
+ }
330
+ ];
331
+
332
+ return {
333
+ /**
334
+ * @param code video categoryId
335
+ * @returns category title
336
+ */
337
+ lookup: function (code) {
338
+ if (!code) {
339
+ return;
340
+ }
341
+ for (let i = 0; i < categories.length; i++) {
342
+ const category = categories[i];
343
+
344
+ if (Number(category.id) === Number(code)) {
345
+ return category.snippet.title;
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }());