├── LICENSE ├── README.md ├── gdriveDL.js └── itag.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lylia 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gdriveDL V1.0 2 | Welcome to gdriveDL V1.0! This application allows you to download files from Google Drive using their API. 3 | 4 | ## Setup 5 | 6 | 1. Create a new project in the [Google Developers Console](https://console.developers.google.com/). 7 | 2. Under "Credentials", create a new OAuth 2.0 Client ID. 8 | 3. Set the "Authorized redirect URIs" field to `https://sub.domain.workers.dev/oauth2/callback`. 9 | 4. Click "Create". 10 | 5. In `gdriveDL.js`, replace the empty strings in `const CLIENT_ID` and `const CLIENT_SECRET` with your client ID and secret, respectively. 11 | 6. Deploy the application to [Cloudflare Workers](https://workers.cloudflare.com/). 12 | 7. Congrats! Your gdriveDL instance is now ready to use. 13 | 14 | ## Usage 15 | 16 | Once you have set up your gdriveDL instance, you can start using it to download files from Google Drive. 17 | 18 | ### Authenticate 19 | 20 | Before downloading files, you will need to authenticate. To do so, visit the authorization URL at `https://your-instance-name.your-account.workers.dev/`. 21 | 22 | ### Download 23 | 24 | To download a file, make a GET request to `https://your-instance-name.your-account.workers.dev/api/v1/download?fileId=[FILE_ID]`. Replace `[FILE_ID]` with the ID of the Google Drive file you want to download. 25 | 26 | You may also add an `access_token` parameter to the query string if you already have a valid access token. 27 | 28 | ## Notes 29 | 30 | - If the user is not authenticated, they will be redirected to the authorization URL. 31 | - If the user is authenticated but the access token has expired, the application will attempt to refresh it using the refresh token. 32 | - If there is an error downloading a file, an error message will be logged in the console. 33 | -------------------------------------------------------------------------------- /gdriveDL.js: -------------------------------------------------------------------------------- 1 | /* 2 | gdriveDL V1.0 3 | MIT License 4 | 5 | Copyright (c) 2023 IsThisUser 6 | 7 | */ 8 | // Constants 9 | const CLIENT_ID = ''; 10 | const CLIENT_SECRET = ''; 11 | const REDIRECT_URI = 'https://sub.domain.workers.dev/oauth2/callback'; 12 | const AUTHORIZATION_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth'; 13 | const TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token'; 14 | const SCOPES = 'https://www.googleapis.com/auth/drive'; 15 | 16 | // Handle incoming requests 17 | addEventListener('fetch', event => { 18 | const url = new URL(event.request.url); 19 | if (url.pathname.startsWith('/oauth2/callback')) { 20 | event.respondWith(handleCallback(event.request)); 21 | } else { 22 | event.respondWith(handleRequest(event.request)); 23 | } 24 | }); 25 | 26 | async function handleRequest(request) { 27 | const url = new URL(request.url); 28 | const path = url.pathname; 29 | 30 | // If the user is not authenticated, redirect them to the authorization page 31 | if (!isAuthenticated(request)) { 32 | const authorizationUrl = await getAuthorizationUrl(); 33 | return Response.redirect(authorizationUrl, 302); 34 | } 35 | 36 | // If the request is just for the root path, provide some basic info about available downloads 37 | if (path === '/') { 38 | const responseBody = { 39 | message: 'You are logged in! Here are some available api routes', 40 | download: '/api/v1/download?fileId=xxx' 41 | }; 42 | const response = new Response(JSON.stringify(responseBody)); 43 | response.headers.set('Content-Type', 'application/json'); 44 | return response; 45 | } 46 | 47 | // If the user is authenticated and requesting a file download, download the file using their access token 48 | if (path.toLowerCase().startsWith('/api/v1/download')) { 49 | const fileId = url.searchParams.get('fileId'); 50 | const accessToken = url.searchParams.get('access_token') || getToken(request).access_token; 51 | const drive = new googleDrive(accessToken); 52 | if (accessToken !== null) { 53 | return await drive.downloadFile(fileId, accessToken); 54 | } 55 | } 56 | 57 | // If the request doesn't match any of the above handlers, return a 404 response 58 | return new Response('Not Found', { status: 404 }); 59 | } 60 | 61 | async function handleCallback(request) { 62 | const url = new URL(request.url); 63 | const code = url.searchParams.get('code'); 64 | if (code !== null && code !== undefined) { 65 | const accessToken = await getAccessToken(code); 66 | const responseBody = { 67 | access_token: accessToken.access_token, 68 | expires_in: accessToken.expires_in, 69 | refresh_token: accessToken.refresh_token 70 | }; 71 | const response = new Response(JSON.stringify(responseBody)); 72 | const accessTokenExpiration = Date.now() + accessToken.expires_in * 1000; 73 | response.headers.set('Set-Cookie', `access_token=${accessToken.access_token}; Secure; HttpOnly; SameSite=None; Expires=${new Date(accessTokenExpiration).toUTCString()}; Path=/`); 74 | const refreshTokenExpiration = Date.now() + 60 * 24 * 60 * 60 * 1000; // 60 days 75 | response.headers.append('Set-Cookie', `refresh_token=${accessToken.refresh_token}; Secure; HttpOnly; SameSite=None; Expires=${new Date(refreshTokenExpiration).toUTCString()}; Path=/`); 76 | response.headers.set('Content-Type', 'application/json'); 77 | return response; 78 | } else { 79 | return new Response('Authorization failed', { status: 401 }); 80 | } 81 | } 82 | 83 | // Returns true if the request has an authenticated user, false otherwise 84 | function isAuthenticated(request) { 85 | const token = getToken(request); 86 | return token !== null && token !== undefined; 87 | } 88 | 89 | // Gets the access token from the request headers or cookies, if present 90 | function getToken(request) { 91 | const url = new URL(request.url); 92 | const accessTokenFromQuery = url.searchParams.get('access_token'); 93 | if (accessTokenFromQuery !== null) { 94 | return { access_token: accessTokenFromQuery }; 95 | } 96 | const authorizationHeader = request.headers.get('Authorization'); 97 | if (authorizationHeader !== null && authorizationHeader.startsWith('Bearer ')) { 98 | const accessToken = authorizationHeader.substring('Bearer '.length); 99 | return { access_token: accessToken }; 100 | } else { 101 | const cookieHeader = request.headers.get('Cookie'); 102 | if (cookieHeader !== null && cookieHeader.includes('access_token=')) { 103 | const cookies = cookieHeader.split(';'); 104 | for (const cookie of cookies) { 105 | const [name, value] = cookie.trim().split('='); 106 | if (name === 'access_token') { 107 | return { access_token: value }; 108 | } 109 | } 110 | } 111 | return null; 112 | } 113 | } 114 | 115 | // Gets the URL of the Authorization endpoint to redirect users to for authentication 116 | async function getAuthorizationUrl() { 117 | const url = new URL(AUTHORIZATION_ENDPOINT); 118 | url.searchParams.append('client_id', CLIENT_ID); 119 | url.searchParams.append('redirect_uri', REDIRECT_URI); 120 | url.searchParams.append('response_type', 'code'); 121 | url.searchParams.append('scope', SCOPES); 122 | url.searchParams.append('access_type', 'offline'); 123 | url.searchParams.append('prompt', 'consent'); 124 | return url.toString(); 125 | } 126 | 127 | // Exchanges an authorization code for an access token and refresh token 128 | async function getAccessToken(authorizationCode) { 129 | const headers = { 130 | 'Content-Type': 'application/x-www-form-urlencoded' 131 | }; 132 | const body = new URLSearchParams({ 133 | grant_type: 'authorization_code', 134 | client_id: CLIENT_ID, 135 | client_secret: CLIENT_SECRET, 136 | redirect_uri: REDIRECT_URI, // Use REDIRECT_URI directly 137 | code: authorizationCode 138 | }); 139 | const response = await fetch(TOKEN_ENDPOINT, { method: 'POST', headers: headers, body: body }); 140 | const json = await response.json(); 141 | return { 142 | access_token: json.access_token, 143 | refresh_token: json.refresh_token, 144 | expires_in: json.expires_in 145 | }; 146 | } 147 | 148 | // Refreshes the access token using the provided refresh token 149 | async function refreshAccessToken(refreshToken) { 150 | const headers = { 151 | 'Content-Type': 'application/x-www-form-urlencoded' 152 | }; 153 | const body = new URLSearchParams({ 154 | grant_type: 'refresh_token', 155 | client_id: CLIENT_ID, 156 | client_secret: CLIENT_SECRET, 157 | refresh_token: refreshToken 158 | }); 159 | const response = await fetch(TOKEN_ENDPOINT, { method: 'POST', headers: headers, body: body }); 160 | const json = await response.json(); 161 | return { 162 | access_token: json.access_token, 163 | expires_in: json.expires_in 164 | }; 165 | } 166 | 167 | // Downloads a file from Google Drive using the specified access token and file ID 168 | class googleDrive { 169 | constructor(accessToken, refreshToken) { 170 | this.accessToken = accessToken; 171 | this.refreshToken = refreshToken; 172 | } 173 | 174 | async downloadFile(fileId) { 175 | try { 176 | const requestOption = { 177 | method: 'GET', 178 | headers: { Authorization: `Bearer ${this.accessToken}` } 179 | }; 180 | const response = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, requestOption); 181 | if (response.status === 401 && this.refreshToken !== undefined) { 182 | // Access token has expired, try refreshing it with the refresh token 183 | const newAccessToken = await refreshAccessToken(this.refreshToken); 184 | this.accessToken = newAccessToken.access_token; 185 | console.log(`Access token refreshed, new expiration: ${newAccessToken.expires_in} seconds`); 186 | // Retry the file download with the new access token 187 | const requestOption = { 188 | method: 'GET', 189 | headers: { Authorization: `Bearer ${this.accessToken}` } 190 | }; 191 | const response = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, requestOption); 192 | return response; 193 | } 194 | return response; 195 | } catch (e) { 196 | console.log(e); 197 | throw e; 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /itag.js: -------------------------------------------------------------------------------- 1 | addEventListener('fetch', event => { 2 | event.respondWith(handleRequest(event.request)) 3 | }) 4 | 5 | const itagResolutionMap = { 6 | '5': '240', '6': '270', '17': '144', '18': '360', '22': '720', '34': '360', '35': '480', 7 | '36': '240', '37': '1080', '38': '3072', '43': '360', '44': '480', '45': '720', '46': '1080', 8 | '82': '360 [3D]', '83': '480 [3D]', '84': '720 [3D]', '85': '1080p [3D]', '100': '360 [3D]', 9 | '101': '480 [3D]', '102': '720 [3D]', '92': '240', '93': '360', '94': '480', '95': '720', 10 | '96': '1080', '132': '240', '151': '72', '133': '240', '134': '360', '135': '480', 11 | '136': '720', '137': '1080', '138': '2160', '160': '144', '264': '1440', 12 | '298': '720', '299': '1080', '266': '2160', '167': '360', '168': '480', '169': '720', 13 | '170': '1080', '218': '480', '219': '480', '242': '240', '243': '360', '244': '480', 14 | '245': '480', '246': '480', '247': '720', '248': '1080', '271': '1440', '272': '2160', 15 | '302': '2160', '303': '1080', '308': '1440', '313': '2160', '315': '2160', '59': '480' 16 | }; 17 | 18 | async function handleRequest(request) { 19 | const file_id = new URL(request.url).searchParams.get('file_id'); 20 | if (!file_id) { 21 | return new Response('Error: missing file_id parameter', {status: 400}); 22 | } 23 | 24 | let accessTokenInfo = getAccessToken(request); 25 | if (accessTokenInfo === null) { 26 | const newAccessToken = await refreshAccessToken(R_TOKEN); 27 | accessTokenInfo = newAccessToken; 28 | } 29 | 30 | // Access token is still valid 31 | const acc_token = accessTokenInfo.access_token; 32 | 33 | const isVideo = await isVideoFile(acc_token, file_id); 34 | 35 | if (isVideo) { 36 | // Perform both direct and transcoded downloads 37 | const directUrl = await getDirectDownloadUrl(acc_token, file_id); 38 | const transcodedUrls = await getTranscodedDownloadUrls(acc_token, file_id); 39 | 40 | if (directUrl === null && Object.keys(transcodedUrls).length === 0) { 41 | return new Response('Failed to retrieve download link T', {status: 400}); 42 | } 43 | 44 | let downloadButtons = ''; 45 | if (directUrl) { 46 | downloadButtons += `
Download Direct
`; 47 | } 48 | for (const itag in transcodedUrls) { 49 | const url = transcodedUrls[itag].url; 50 | const resolution = transcodedUrls[itag].resolution; 51 | downloadButtons += `
Download ${resolution}p
`; 52 | } 53 | const Responsex = new Response(generateHtml(downloadButtons, directUrl.title)); 54 | Responsex.headers.append('Set-Cookie', `access_token=${acc_token}; path=/; HttpOnly; SameSite=Strict`); 55 | Responsex.headers.set('Content-Type', 'text/html'); 56 | return Responsex 57 | } else { 58 | // Perform direct download only 59 | const url = await getDirectDownloadUrl(acc_token, file_id); 60 | if (!url) { 61 | return new Response('FaiIled to retrieve download link', {status: 400}); 62 | } 63 | let downloadButtons = ''; 64 | downloadButtons += `
Download Direct
`; 65 | const Responsex = new Response(generateHtml(downloadButtons, url.title)); 66 | Responsex.headers.append('Set-Cookie', `access_token=${acc_token}; path=/; HttpOnly; SameSite=Strict`); 67 | Responsex.headers.set('Content-Type', 'text/html'); 68 | return Responsex 69 | } 70 | } 71 | 72 | async function isVideoFile(access_token, file_id) { 73 | try { 74 | const response = await fetch(`https://www.googleapis.com/drive/v3/files/${file_id}?supportsAllDrives=true&includeItemsFromAllDrives=true&fields=name,mimeType`, { 75 | headers: { 76 | "Authorization": `Bearer ${access_token}` 77 | } 78 | }); 79 | const json = await response.text(); 80 | const parsedJson = JSON.parse(json); 81 | if (parsedJson.mimeType && parsedJson.mimeType.startsWith('video/')) { 82 | return true; 83 | } else { 84 | return false; 85 | } 86 | } catch (error) { 87 | return false; 88 | } 89 | } 90 | 91 | async function getDirectDownloadUrl(acc_token, file_id) { 92 | // Check file existence and access 93 | const resp = await fetch(`https://www.googleapis.com/drive/v3/files/${file_id}?supportsAllDrives=true&includeItemsFromAllDrives=true&fields=name`, { 94 | method: 'GET', 95 | headers: { 96 | 'Authorization': `Bearer ${acc_token}` 97 | } 98 | }); 99 | 100 | // Get direct download URL and file name 101 | const session = { 102 | access_token: acc_token, 103 | "client_id": CLIENT_ID, 104 | "client_secret": CLIENT_SECRET, 105 | "refresh_token": REFRESH_TOKEN, 106 | "token_expiry": "", 107 | }; 108 | if (!resp.ok) { 109 | return null; 110 | } 111 | const jsonx = await resp.text(); 112 | const parsedJson = JSON.parse(jsonx); 113 | session.url = `https://www.googleapis.com/drive/v3/files/${file_id}?alt=media`; 114 | const sessionB64 = btoa(JSON.stringify(session)); 115 | const downloadUrl = `https://sub.domain.workers.dev/api/v1/download?session=${sessionB64}`; 116 | 117 | // Extract the file name from the response JSON 118 | const title = parsedJson.name; 119 | 120 | // Return an object with the title and download URL 121 | return { title, downloadUrl }; 122 | } 123 | 124 | async function getTranscodedDownloadUrls(acc_token, file_id) { 125 | const response = await fetch(`https://drive.google.com/get_video_info?docid=${file_id}`, { 126 | headers: { 127 | "Authorization": `Bearer ${acc_token}` 128 | } 129 | }); 130 | const video_info = await response.text(); 131 | const video_params = new URLSearchParams(video_info); 132 | const urls = []; 133 | 134 | if (video_params.has('url_encoded_fmt_stream_map')) { 135 | urls.push(...video_params.get('url_encoded_fmt_stream_map').split(',')); 136 | } 137 | if (video_params.has('adaptive_fmts')) { 138 | urls.push(...video_params.get('adaptive_fmts').split(',')); 139 | } 140 | 141 | const session = { 142 | access_token: acc_token, 143 | "client_id": CLIENT_ID, 144 | "client_secret": CLIENT_SECRET, 145 | "refresh_token": REFRESH_TOKEN, 146 | "token_expiry": "", 147 | }; 148 | const transcodedUrls = {}; 149 | for (const urlString of urls) { 150 | const url = new URLSearchParams(urlString); 151 | const itag = url.get('itag'); 152 | const resolution = itagResolutionMap[itag] || 'Unknown'; 153 | session.url = `${url.get('url')}&${url.get('s') || url.get('sig')}`; 154 | session.cookie = response.headers.get('set-cookie'); 155 | session.transcoded = true; 156 | const sessionB64 = btoa(JSON.stringify(session)); 157 | const downloadUrl = `https://sub.domain.workers.dev/api/v1/download?session=${sessionB64}`; 158 | transcodedUrls[itag] = { 159 | url: downloadUrl, 160 | resolution: resolution, 161 | transcoded: true, 162 | }; 163 | } 164 | return transcodedUrls; 165 | } 166 | 167 | function generateHtml(downloadButtons, title) { 168 | return ` 169 | 170 | 171 | 172 | 173 | 174 | ${title} 175 | 208 | 209 | 210 |
211 |

${title}

212 |

Click below to Download

213 | 216 |
217 | 218 | `; 219 | } 220 | 221 | function enQuery(data) { 222 | const ret = []; 223 | for (let d in data) { 224 | ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d])); 225 | } 226 | return ret.join("&"); 227 | } 228 | 229 | function getAccessToken(request) { 230 | const url = new URL(request.url); 231 | const cookieHeader = request.headers.get('Cookie'); 232 | let accessToken = null; 233 | let accessTokenExpiresIn = null; 234 | if (cookieHeader !== null && cookieHeader.includes('access_token=')) { 235 | const cookies = cookieHeader.split(';'); 236 | for (const cookie of cookies) { 237 | const [name, value] = cookie.trim().split('='); 238 | if (name === 'access_token') { 239 | accessToken = value; 240 | } 241 | } 242 | } 243 | if (accessToken !== null) { 244 | return { access_token: accessToken }; 245 | } 246 | return null; 247 | } 248 | 249 | // Refreshes the access token using the provided refresh token 250 | async function refreshAccessToken(refreshToken) { 251 | const headers = { 252 | 'Content-Type': 'application/x-www-form-urlencoded' 253 | }; 254 | const body = new URLSearchParams({ 255 | grant_type: 'refresh_token', 256 | client_id: CLIENT_ID, 257 | client_secret: CLIENT_SECRET, 258 | refresh_token: refreshToken 259 | }); 260 | const response = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: headers, body: body }); 261 | const json = await response.json(); 262 | if (!response.ok) { 263 | throw new Error(`Error refreshing access token: ${json.error}`); 264 | } 265 | return { 266 | access_token: json.access_token, 267 | expires_in: json.expires_in 268 | }; 269 | } 270 | --------------------------------------------------------------------------------