getMaterial method
- MaterialRefDto material
Fetches download information for a specific course material.
Returns the direct download URL and optional referer header required to download the material file.
The material should be obtained from getMaterials.
The download process varies by material type:
- Standard files: Direct download URL
- PDFs: Requires a referer URL for access
- Course recordings: Returns iStream URL with
streamable: true
When the returned MaterialDto has a non-null referer field, it must
be included as the Referer header when downloading the file.
Throws an Exception if the material cannot be accessed or parsed.
Implementation
Future<MaterialDto> getMaterial(
MaterialRefDto material,
) async {
await _selectCourse(material.course);
// Step 1: Get launch.php to extract the course ID (cid)
final launchResponse = await _iSchoolPlusDio.get('path/launch.php');
// Extract cid from the JavaScript
// e.g.: parent.s_catalog.location.replace('/learn/path/manifest.php?cid=...')
final cidMatch = RegExp(r"cid=([^']+)").firstMatch(launchResponse.data);
if (cidMatch == null) {
throw Exception('Could not extract course ID from launch page.');
}
final cid = cidMatch.group(1)!;
// Step 2: Get resource token from the course material tree endpoint
// It contains a form with a token needed to fetch downloadable resources
final materialTreeResponse = await _iSchoolPlusDio.get(
'path/pathtree.php',
queryParameters: {'cid': cid},
);
// Extract the read_key token from the HTML form
final materialTreeDocument = parse(materialTreeResponse.data);
final readKeyInput = materialTreeDocument.querySelector(
'#fetchResourceForm>input[name="read_key"][value]',
);
if (readKeyInput == null) {
throw Exception('Could not find read_key in material tree page.');
}
final fetchResourceToken = readKeyInput.attributes['value']!;
// Step 3: Submit resource form and get resource URI
final dioWithoutRedirects = _iSchoolPlusDio.clone()
..interceptors.removeWhere(
(interceptor) => interceptor is RedirectInterceptor,
);
final resourceResponse = await dioWithoutRedirects.post(
'path/SCORM_fetchResource.php',
data: {
'href': '@${material.href!}',
'course_id': cid,
'read_key': fetchResourceToken,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
// Case 1: Response is a redirect
// Replace preview URL with download URL
if (resourceResponse.statusCode == HttpStatus.found) {
final location =
resourceResponse.headers[HttpHeaders.locationHeader]?.first;
if (location == null) {
throw Exception('Redirect location header is missing.');
}
final previewUri = Uri.tryParse(location);
if (previewUri == null) {
throw Exception('Invalid redirect URI: $location');
}
return (
downloadUrl: previewUri.replace(path: "download.php"),
referer: null,
streamable: false,
);
}
// Response is HTML with embedded download script, e.g.,
// <script>location.replace("viewPDF.php?id=KheOh_TuNgPJOQTEmRW1zg,,");</script>
// URI can be enclosed in either single or double quotes
final quoteRegExp = RegExp(r'''(['"])([^'"]+)\1''');
final quoteMatch = quoteRegExp.firstMatch(resourceResponse.data);
if (quoteMatch == null || quoteMatch.groupCount < 2) {
throw Exception('Could not extract download URI from response.');
}
// URI can be relative, so resolve against base URL
final baseUrl = '${_iSchoolPlusDio.options.baseUrl}path/';
final downloadUri = Uri.parse(baseUrl).resolve(quoteMatch.group(2)!);
// Case 2: Material is a course recording
if (downloadUri.host.contains("istream.ntut.edu.tw")) {
// iStream videos can be streamed directly or downloaded
// Testing confirmed no referer required
return (
downloadUrl: downloadUri,
referer: null,
streamable: true,
);
}
// Case 3: Material is a PDF
if (downloadUri.path.contains('viewPDF.php')) {
// Fetch and find the value of DEFAULT_URL in JavaScript
final viewPdfResponse = await _iSchoolPlusDio.getUri(downloadUri);
final defaultUrlRegExp = RegExp(r'DEFAULT_URL[ =]+\"(.+)\"');
final defaultUrlMatch = defaultUrlRegExp.firstMatch(viewPdfResponse.data);
if (defaultUrlMatch == null || defaultUrlMatch.groupCount < 1) {
throw Exception('Could not find DEFAULT_URL in PDF viewer page.');
}
final defaultUrl = defaultUrlMatch.group(1)!;
return (
downloadUrl: Uri.parse(baseUrl).resolve(defaultUrl),
referer: downloadUri.toString(),
streamable: false,
);
}
// Case 4: Material is a standard downloadable file
return (
downloadUrl: downloadUri,
referer: null,
streamable: false,
);
}