Repository: yt-dlp/yt-dlp

Issue:
LinkedIn videos not supported? [Site Request]
<!--

######################################################################
  WARNING!
  IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################

-->


## Checklist

<!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
- Make sure that site you are requesting is not dedicated to copyright infringement, see https://github.com/yt-dlp/yt-dlp. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
- Search the bugtracker for similar site support requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->

- [x] I'm reporting a new site support request
- [x] I've verified that I'm running yt-dlp version **2021.09.25**
- [x] I've checked that all provided URLs are alive and playable in a browser
- [x] I've checked that none of provided URLs violate any copyrights
- [x] The provided URLs do not contain any DRM to the best of my knowledge
- [x] I've searched the bugtracker for similar site support requests including closed ones


## Example URLs

<!--
Provide all kinds of example URLs support for which should be included. Replace following example URLs by yours.
-->

- Single video: https://www.linkedin.com/posts/mishalkhawaja_sendinblueviews-toronto-digitalmarketing-ugcPost-6850898786781339649-mM20

## Description

<!--
Provide any additional information.
If work on your issue requires account credentials please provide them or explain how one can obtain them.
-->

Is LinkedIn not supported? The reason I ask is that I am able to download LinkedIn Learning videos using a cookie file but not LinkedIn feed videos, like the one I have referenced above.

Here's the verbose log:

```
yt-dlp -J https://www.linkedin.com/posts/fordnation_shubnavratri-hindu-navatri-ugcPost-6852030793657794561-4UPb --verbose
[debug] Command-line config: ['-J', 'https://www.linkedin.com/posts/fordnation_shubnavratri-hindu-navatri-ugcPost-6852030793657794561-4UPb', '--verbose']
[debug] Encodings: locale UTF-8, fs utf-8, out UTF-8, pref UTF-8
[debug] yt-dlp version 2021.09.25
[debug] Python version 3.7.5 (CPython 64bit) - Linux-4.15.0-135-generic-x86_64-with-Ubuntu-18.04-bionic
[debug] exe versions: none
[debug] Optional libraries: Crypto, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {}
[debug] [generic] Extracting URL: https://www.linkedin.com/posts/fordnation_shubnavratri-hindu-navatri-ugcPost-6852030793657794561-4UPb
[generic] fordnation_shubnavratri-hindu-navatri-ugcPost-6852030793657794561-4UPb: Requesting header
WARNING: [generic] Falling back on generic information extractor.
[generic] fordnation_shubnavratri-hindu-navatri-ugcPost-6852030793657794561-4UPb: Downloading webpage
[generic] fordnation_shubnavratri-hindu-navatri-ugcPost-6852030793657794561-4UPb: Extracting information
[debug] [generic] Extracting URL: https://www.linkedin.com/embed/feed/update/urn:li:activity:6852030803648622592?compact=true
[generic] urn:li:activity:6852030803648622592?compact=true: Requesting header
WARNING: [generic] Falling back on generic information extractor.
[generic] urn:li:activity:6852030803648622592?compact=true: Downloading webpage
[generic] urn:li:activity:6852030803648622592?compact=true: Extracting information
ERROR: [generic] Unsupported URL: https://www.linkedin.com/embed/feed/update/urn:li:activity:6852030803648622592?compact=true
Traceback (most recent call last):
  File "/home/bd7/.local/lib/python3.7/site-packages/yt_dlp/extractor/common.py", line 585, in extract
    ie_result = self._real_extract(url)
  File "/home/bd7/.local/lib/python3.7/site-packages/yt_dlp/extractor/generic.py", line 3712, in _real_extract
    raise UnsupportedError(url)
yt_dlp.utils.UnsupportedError: Unsupported URL: https://www.linkedin.com/embed/feed/update/urn:li:activity:6852030803648622592?compact=true
Traceback (most recent call last):
  File "/home/bd7/.local/lib/python3.7/site-packages/yt_dlp/extractor/common.py", line 585, in extract
    ie_result = self._real_extract(url)
  File "/home/bd7/.local/lib/python3.7/site-packages/yt_dlp/extractor/generic.py", line 3712, in _real_extract
    raise UnsupportedError(url)
yt_dlp.utils.UnsupportedError: Unsupported URL: https://www.linkedin.com/embed/feed/update/urn:li:activity:6852030803648622592?compact=true

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/bd7/.local/lib/python3.7/site-packages/yt_dlp/YoutubeDL.py", line 1227, in wrapper
    return func(self, *args, **kwargs)
  File "/home/bd7/.local/lib/python3.7/site-packages/yt_dlp/YoutubeDL.py", line 1252, in __extract_info
    ie_result = ie.extract(url)
  File "/home/bd7/.local/lib/python3.7/site-packages/yt_dlp/extractor/common.py", line 602, in extract
    e.msg, video_id=video_id, ie=self.IE_NAME, tb=e.traceback, expected=e.expected, cause=e.cause)
yt_dlp.utils.ExtractorError: [generic] Unsupported URL: https://www.linkedin.com/embed/feed/update/urn:li:activity:6852030803648622592?compact=true
```



Discussion:
I got some linkedin videos to download using this command:
`yt-dlp --cookies "C:\Users\username\Downloads\linkedin.com_cookies.txt" -o "C:\Temp\Training Videos\LinkedIn Learning\Powershell\%(playlist_title)s\Lesson %(chapter_number)02d - %(chapter)s\%(playlist_index)s - %(title)s.%(ext)s.mp4" https://www.linkedin.com/learning/powershell-7-essential-training. `

Here is something that was in my notes from a previous script:
```
https://github.com/ytdl-org/youtube-dl/issues/20326

Use Chrome to login to linkedin:learning
    Play a learning video e.g. on this page.
    Use the cookies plugin to export the file cookies.txt

How to get the "x-li-identity” header value
    In Chrome press F12 to open the Network Monitor (development console).
    Reload the linkedin:learning page with the video.
    In the Network Monitor, search for “/learning-api”, and click on one of the lines with the HTTP requests.
    On the right side, scroll down to the request header line “x-li-identity: ...”, and copy 
    the value to your command line option: --add-header "x-li-identity:..."

:start
yt-dlp --cookies "linkedin.com_cookies.txt" --add-header "x-li-identity:dXJuOmxpOmVudGVycHJpc2VQcm9maWxlOih1cm46bGk6ZW50ZXJwcmlzZUFjY291bnQ6MjEzMzg0OSwxMzcwNDk0NjAp" -o "C:\Temp\Training Videos\LinkedIn Learning\%(playlist_title)s\Lesson %(chapter_number)02d - %(chapter)s\%(playlist_index)s - %(title)s.%(ext)s.mp4" https://www.linkedin.com/learning/programming-foundations-databases
```

I'm not sure if the "x-li-identity:" is still needed, as I didn't need it this morning. Hope this helps.
> I got some linkedin videos to download using this command: `yt-dlp --cookies "C:\Users\username\Downloads\linkedin.com_cookies.txt" -o "C:\Temp\Training Videos\LinkedIn Learning\Powershell\%(playlist_title)s\Lesson %(chapter_number)02d - %(chapter)s\%(playlist_index)s - %(title)s.%(ext)s.mp4" https://www.linkedin.com/learning/powershell-7-essential-training. `
> 
> Here is something that was in my notes from a previous script:
> 
> ```
> https://github.com/ytdl-org/youtube-dl/issues/20326
> 
> Use Chrome to login to linkedin:learning
>     Play a learning video e.g. on this page.
>     Use the cookies plugin to export the file cookies.txt
> 
> How to get the "x-li-identity” header value
>     In Chrome press F12 to open the Network Monitor (development console).
>     Reload the linkedin:learning page with the video.
>     In the Network Monitor, search for “/learning-api”, and click on one of the lines with the HTTP requests.
>     On the right side, scroll down to the request header line “x-li-identity: ...”, and copy 
>     the value to your command line option: --add-header "x-li-identity:..."
> 
> :start
> yt-dlp --cookies "linkedin.com_cookies.txt" --add-header "x-li-identity:dXJuOmxpOmVudGVycHJpc2VQcm9maWxlOih1cm46bGk6ZW50ZXJwcmlzZUFjY291bnQ6MjEzMzg0OSwxMzcwNDk0NjAp" -o "C:\Temp\Training Videos\LinkedIn Learning\%(playlist_title)s\Lesson %(chapter_number)02d - %(chapter)s\%(playlist_index)s - %(title)s.%(ext)s.mp4" https://www.linkedin.com/learning/programming-foundations-databases
> ```
> 
> I'm not sure if the "x-li-identity:" is still needed, as I didn't need it this morning. Hope this helps.

That video is from a LinkedIn Learning course. The issue that I've described is for Linkedin videos that appear in the feed. Those videos cannot be downloaded by yt-dlp at the moment, even when using cookies.

Resolving patch:
diff --git a/yt_dlp/extractor/extractors.py b/yt_dlp/extractor/extractors.py
--- a/yt_dlp/extractor/extractors.py
+++ b/yt_dlp/extractor/extractors.py
@@ -698,6 +698,7 @@
     LineLiveChannelIE,
 )
 from .linkedin import (
+    LinkedInIE,
     LinkedInLearningIE,
     LinkedInLearningCourseIE,
 )
diff --git a/yt_dlp/extractor/linkedin.py b/yt_dlp/extractor/linkedin.py
--- a/yt_dlp/extractor/linkedin.py
+++ b/yt_dlp/extractor/linkedin.py
@@ -6,21 +6,56 @@
 
 from .common import InfoExtractor
 from ..utils import (
+    clean_html,
+    extract_attributes,
     ExtractorError,
     float_or_none,
+    get_element_by_class,
     int_or_none,
     srt_subtitles_timecode,
+    strip_or_none,
+    mimetype2ext,
     try_get,
     urlencode_postdata,
     urljoin,
 )
 
 
-class LinkedInLearningBaseIE(InfoExtractor):
+class LinkedInBaseIE(InfoExtractor):
     _NETRC_MACHINE = 'linkedin'
-    _LOGIN_URL = 'https://www.linkedin.com/uas/login?trk=learning'
     _logged_in = False
 
+    def _real_initialize(self):
+        if self._logged_in:
+            return
+        email, password = self._get_login_info()
+        if email is None:
+            return
+
+        login_page = self._download_webpage(
+            self._LOGIN_URL, None, 'Downloading login page')
+        action_url = urljoin(self._LOGIN_URL, self._search_regex(
+            r'<form[^>]+action=(["\'])(?P<url>.+?)\1', login_page, 'post url',
+            default='https://www.linkedin.com/uas/login-submit', group='url'))
+        data = self._hidden_inputs(login_page)
+        data.update({
+            'session_key': email,
+            'session_password': password,
+        })
+        login_submit_page = self._download_webpage(
+            action_url, None, 'Logging in',
+            data=urlencode_postdata(data))
+        error = self._search_regex(
+            r'<span[^>]+class="error"[^>]*>\s*(.+?)\s*</span>',
+            login_submit_page, 'error', default=None)
+        if error:
+            raise ExtractorError(error, expected=True)
+        LinkedInBaseIE._logged_in = True
+
+
+class LinkedInLearningBaseIE(LinkedInBaseIE):
+    _LOGIN_URL = 'https://www.linkedin.com/uas/login?trk=learning'
+
     def _call_api(self, course_slug, fields, video_slug=None, resolution=None):
         query = {
             'courseSlug': course_slug,
@@ -52,32 +87,47 @@ def _get_urn_id(self, video_data):
     def _get_video_id(self, video_data, course_slug, video_slug):
         return self._get_urn_id(video_data) or '%s/%s' % (course_slug, video_slug)
 
-    def _real_initialize(self):
-        if self._logged_in:
-            return
-        email, password = self._get_login_info()
-        if email is None:
-            return
 
-        login_page = self._download_webpage(
-            self._LOGIN_URL, None, 'Downloading login page')
-        action_url = urljoin(self._LOGIN_URL, self._search_regex(
-            r'<form[^>]+action=(["\'])(?P<url>.+?)\1', login_page, 'post url',
-            default='https://www.linkedin.com/uas/login-submit', group='url'))
-        data = self._hidden_inputs(login_page)
-        data.update({
-            'session_key': email,
-            'session_password': password,
-        })
-        login_submit_page = self._download_webpage(
-            action_url, None, 'Logging in',
-            data=urlencode_postdata(data))
-        error = self._search_regex(
-            r'<span[^>]+class="error"[^>]*>\s*(.+?)\s*</span>',
-            login_submit_page, 'error', default=None)
-        if error:
-            raise ExtractorError(error, expected=True)
-        LinkedInLearningBaseIE._logged_in = True
+class LinkedInIE(LinkedInBaseIE):
+    _VALID_URL = r'https?://(?:www\.)?linkedin\.com/posts/.+?(?P<id>\d+)'
+    _TESTS = [{
+        'url': 'https://www.linkedin.com/posts/mishalkhawaja_sendinblueviews-toronto-digitalmarketing-ugcPost-6850898786781339649-mM20',
+        'info_dict': {
+            'id': '6850898786781339649',
+            'ext': 'mp4',
+            'title': 'Mishal K. on LinkedIn: #sendinblueviews #toronto #digitalmarketing',
+            'description': 'md5:be125430bab1c574f16aeb186a4d5b19',
+            'creator': 'Mishal K.'
+        },
+    }]
+
+    def _real_extract(self, url):
+        video_id = self._match_id(url)
+        webpage = self._download_webpage(url, video_id)
+
+        title = self._html_search_regex(r'<title>([^<]+)</title>', webpage, 'title')
+        description = clean_html(get_element_by_class('share-update-card__update-text', webpage))
+        like_count = int_or_none(get_element_by_class('social-counts-reactions__social-counts-numRections', webpage))
+        creator = strip_or_none(clean_html(get_element_by_class('comment__actor-name', webpage)))
+        
+        sources = self._parse_json(extract_attributes(self._search_regex(r'(<video[^>]+>)', webpage, 'video'))['data-sources'], video_id)
+        formats = [{
+            'url': source['src'],
+            'ext': mimetype2ext(source.get('type')),
+            'tbr': float_or_none(source.get('data-bitrate'), scale=1000),
+        } for source in sources]
+
+        self._sort_formats(formats)
+
+        return {
+            'id': video_id,
+            'formats': formats,
+            'title': title,
+            'like_count': like_count,
+            'creator': creator,
+            'thumbnail': self._og_search_thumbnail(webpage),
+            'description': description,
+        }
 
 
 class LinkedInLearningIE(LinkedInLearningBaseIE):
@@ -108,7 +158,6 @@ def json2srt(self, transcript_lines, duration=None):
     def _real_extract(self, url):
         course_slug, video_slug = self._match_valid_url(url).groups()
 
-        video_data = None
         formats = []
         for width, height in ((640, 360), (960, 540), (1280, 720)):
             video_data = self._call_api(


Test patch: